draft Guide to the Secure Configuration of Fedora This guide presents a catalog of security-relevant configuration settings for Fedora. It is a rendering of content structured in the eXtensible Configuration Checklist Description Format (XCCDF) in order to support security automation. The SCAP content is is available in the scap-security-guide package which is developed at https://www.open-scap.org/security-policies/scap-security-guide. Providing system administrators with such guidance informs them how to securely configure systems under their control in a variety of network roles. Policy makers and baseline creators can use this catalog of settings, with its associated references to higher-level security control catalogs, in order to assist them in security baseline creation. This guide is a catalog, not a checklist, and satisfaction of every item is not likely to be possible or sensible in many operational scenarios. However, the XCCDF format enables granular selection and adjustment of settings, and their association with OVAL and OCIL content provides an automated checking capability. Transformations of this document, and its associated automated checking content, are capable of providing baselines that meet a diverse set of policy objectives. Some example XCCDF Profiles, which are selections of items that form checklists and can be used as baselines, are available with this guide. They can be processed, in an automated fashion, with tools that support the Security Content Automation Protocol (SCAP). The DISA STIG, which provides required settings for US Department of Defense systems, is one example of a baseline created from this guidance. Do not attempt to implement any of the settings in this guide without first testing them in a non-operational environment. The creators of this guidance assume no responsibility whatsoever for its use by other parties, and makes no guarantees, expressed or implied, about its quality, reliability, or any other characteristic. The SCAP Security Guide Project https://www.open-scap.org/security-policies/scap-security-guide Red Hat and Red Hat Enterprise Linux are either registered trademarks or trademarks of Red Hat, Inc. in the United States and other countries. All other names are registered trademarks or trademarks of their respective companies. anssi app-srg app-srg-ctr bsi cis cis-csc cjis cobit5 cui dcid disa hipaa isa-62443-2009 isa-62443-2013 ism iso27001-2013 nerc-cip nist nist-csf os-srg ospp pcidss pcidss4 stigid stigref 0.1.78 SCAP Security Guide Project SCAP Security Guide Project Frank J Cameron (CAM1244) <cameron@ctc.com> 0x66656c6978 <0x66656c6978@users.noreply.github.com> Håvard F. Aasen <havard.f.aasen@pfft.no> Armando Acosta <armando.acosta@oracle.com> Jack Adolph <jack.adolph@gmail.com> Edgar Aguilar <edgar.aguilar@oracle.com> akuster <akuster808@gmail.com> Gabe Alford <redhatrises@gmail.com> Firas AlShafei <firas.alshafei@us.abb.com> Rodrigo Alvares <ralvares@redhat.com> am-tux <andrew.miller11@gmail.com> Christopher Anderson <cba@fedoraproject.org> Craig Andrews <candrews@integralblue.com> angystardust <angystardust@users.noreply.github.com> anivan-suse <anastasija.ivanovic@suse.com> anixon-rh <55244503+anixon-rh@users.noreply.github.com> Anna-Koudelkova <akoudelk@redhat.com> Steve Arnold <sarnold@vctlabs.com> Ikko Ashimine <eltociear@gmail.com> Chuck Atkins <chuck.atkins@kitware.com> axuan <axuan@redhat.com> Bharath B <bhb@redhat.com> Ryan Ballanger <root@rballang-admin-2.fastenal.com> Alex Baranowski <alex@euro-linux.com> Eduardo Barretto <eduardo.barretto@canonical.com> Paul Bastide <pbastide@us.ibm.com> Molly Jo Bault <Molly.Jo.Bault@ballardtech.com> Andrew Becker <A-Beck@users.noreply.github.com> Gabriel Becker <ggasparb@redhat.com> BenGui <benoit.guillon1@etu.unilim.fr> Alexander Bergmann <abergmann@suse.com> Eric Berry <eric@approvedworkman.com> Dale Bewley <dale@bewley.net> Jose Luis BG <bgjoseluis@gmail.com> binyanling <binyanling@uniontech.com> Joseph Bisch <joseph.bisch@gmail.com> Jeff Blank <blank@eclipse.ncsc.mil> Olivier Bonhomme <ptitoliv@ptitoliv.net> bontreger <bontreger@users.noreply.github.com> Lance Bragstad <lbragstad@gmail.com> Ted Brunell <tbrunell@redhat.com> Marcus Burghardt <maburgha@redhat.com> Matthew Burket <mburket@redhat.com> Blake Burkhart <blake.burkhart@us.af.mil> Patrick Callahan <pmc@patrickcallahan.com> George Campbell <gcampbell@palantir.com> Nick Carboni <ncarboni@redhat.com> Carlos <64919342+carlosmmatos@users.noreply.github.com> James Cassell <james.cassell@ll.mit.edu> Frank Caviggia <fcaviggia@users.noreply.github.com> Sinong Chen <costinchen@tencent.com> Eric Christensen <echriste@redhat.com> Dan Clark <danclark@redhat.com> Jayson Cofell <1051437+70k10@users.noreply.github.com> David du Colombier <djc@datadoghq.com> Commandcracker <lukas.fricke.dev@gmail.com> Caleb Cooper <coopercd@ornl.gov> CoreyCook8 <129206271+CoreyCook8@users.noreply.github.com> cortesana <acortes@redhat.com> Richard Maciel Costa <richard.maciel.costa@canonical.com> Xavier Coulon <xavier.coulon@suse.com> Deric Crago <deric.crago@gmail.com> crleekwc <crleekwc@gmail.com> cueball23 <christoph.alms@westnetz.de> cyarbrough76 <42849651+cyarbrough76@users.noreply.github.com> Maura Dailey <maura@eclipse.ncsc.mil> Benjamin Deering <ben_deering@jeepingben.net> Klaas Demter <demter@atix.de> denknorr <dennis.knorr@suse.com> dhanushkar-wso2 <dhanushkar@wso2.com> Andrew DiPrinzio <andrew.diprinzio@jhuapl.edu> dom <dominique.blaze@devinci.fr> Jean-Baptiste Donnette <jean-baptiste.donnette@epita.fr> Marco De Donno <mdedonno1337@gmail.com> dperrone <dperrone@redhat.com> drax <applezip@gmail.com> Sebastian Dunne <sdunne@redhat.com> François Duthilleul <francoisduthilleul@gmail.com> Greg Elin <gregelin@gitmachines.com> eradot4027 <jrtonmac@gmail.com> ericeberry <ericeberry@gmail.com> ermeratos <manuel.ermer@eviden.net> Evelyn <evansvevelyn@gmail.com> Alexis Facques <alexis.facques@mythalesgroup.io> Jan Fader <jan.fader@web.de> Henry Finucane <hfinucane@zscaler.com> Leah Fisher <lfisher047@gmail.com> Marco Fortina <marco_fortina@hotmail.it> Yavor Georgiev <strandjata@gmail.com> Alijohn Ghassemlouei <alijohn@secureagc.com> Swarup Ghosh <swghosh@redhat.com> ghylock <ghylock@gmail.com> Andrew Gilmore <agilmore2@gmail.com> Joshua Glemza <jglemza@nasa.gov> Nick Gompper <forestgomp@yahoo.com> David Fernandez Gonzalez <david.fernandezgonzalez@canonical.com> Loren Gordon <lorengordon@users.noreply.github.com> Gene Gotimer <otherdevopsgene@portinfo.com> Patrik Greco <sikevux@sikevux.se> Steve Grubb <sgrubb@redhat.com> guangyee <gyee@suse.com> Bhargavi Gudi <bgudi@bgudi-thinkpadt14sgen2i.remote.csb> Christian Hagenest <christian.hagenest@suse.com> Marek Haicman <mhaicman@redhat.com> Sun, Haoxiang <haoxiang.sun@intel.com> Vern Hart <vern.hart@canonical.com> Alex Haydock <alex@alexhaydock.co.uk> Rebekah Hayes <rhayes@corp.rivierautilities.com> Trey Henefield <thenefield@gmail.com> Henning Henkel <henning.henkel@helvetia.ch> hex2a <hex2a@users.noreply.github.com> hipponix <mirco.santori@gmail.com> John Hooks <jhooks@starscream.pa.jhbcomputers.com> Jakub Hrozek <jhrozek@redhat.com> Donald Hunter <donald.hunter@gmail.com> De Huo <De.Huo@windriver.com> Robin Price II <robin@redhat.com> Yasir Imam <yimam@redhat.com> Jiri Jaburek <jjaburek@redhat.com> Keith Jackson <keithkjackson@gmail.com> Marc Jadoul <mgjadoul@laptomatic.auth-o-matic.corp> Jeremiah Jahn <jeremiah@goodinassociates.com> Jakub Jelen <jjelen@redhat.com> Jessicahfy <Jessicahfy@users.noreply.github.com> Stephan Joerrens <Stephan.Joerrens@fiduciagad.de> Simon John <sjohn@tuxcare.com> Hunter Jones <hjones2199@gmail.com> Jono <jono@ubuntu-18.localdomain> justchris1 <justchris1@justchris1.email> Kacper <kacper@kacper.se> Kai Kang <kai.kang@windriver.com> Charles Kernstock <charles.kernstock@ultra-ats.com> Yuli Khodorkovskiy <ykhodorkovskiy@tresys.com> Sherine Khoury <skhoury@redhat.com> Nathan Kinder <nkinder@redhat.com> Lee Kinser <lee.kinser@gmail.com> Evgeny Kolesnikov <ekolesni@redhat.com> Peter 'Pessoft' Kolínek <github@pessoft.com> Luke Kordell <luke.t.kordell@lmco.com> Malte Kraus <malte.kraus@suse.com> Seth Kress <seth.kress@dsainc.com> Felix Krohn <felix.krohn@helvetia.ch> kspargur <kspargur@kspargur.csb> Amit Kumar <amitkuma@redhat.com> Fen Labalme <fen@civicactions.com> Dexter Le <dexter.le@sap.com> Ade Lee <alee@redhat.com> Christopher Lee <Crleekwc@gmail.com> Ian Lee <lee1001@llnl.gov> Jarrett Lee <jarrettl@umd.edu> Joseph Lenox <joseph.lenox@collins.com> lichtblaugue <guenther.lichtblau@eviden.com> Jan Lieskovsky <jlieskov@redhat.com> Markus Linnala <Markus.Linnala@knowit.fi> Flos Lonicerae <lonicerae@gmail.com> Simon Lukasik <slukasik@redhat.com> Andrew Lukoshko <andrew.lukoshko@gmail.com> Milan Lysonek <mlysonek@redhat.com> Fredrik Lysén <fredrik@pipemore.se> Mackemania <8738793+Mackemania@users.noreply.github.com> Caitlin Macleod <caitelatte@gmail.com> Dmitry Makovey <dmakovey@yahoo.com> Nick Maludy <nmaludy@gmail.com> Lokesh Mandvekar <lsm5@fedoraproject.org> Matus Marhefka <mmarhefk@redhat.com> Jamie Lorwey Martin <jlmartin@redhat.com> Carlos Matos <cmatos@redhat.com> Robert McAllister <rmcallis@redhat.com> Karen McCarron <kmccarro@redhat.com> Michael McConachie <michael@redhat.com> Marcus Meissner <meissner@suse.de> Khary Mendez <kmendez@redhat.com> Rodney Mercer <rmercer@harris.com> Matt Micene <nzwulfin@gmail.com> Brian Millett <bmillett@gmail.com> Takuya Mishina <tmishina@jp.ibm.com> Mixer9 <35545791+Mixer9@users.noreply.github.com> mmosel <mmosel@kde.example.com> Thomas Montague <montague.thomas@gmail.com> Alan Moore <alan.moore@canonical.com> Zbynek Moravec <zmoravec@redhat.com> Kazuo Moriwaka <moriwaka@users.noreply.github.com> Michael Moseley <michael@eclipse.ncsc.mil> Nathan Moyer <nmoyer@spectric.com> Ross Murphy <RossMurphy@ibm.com> Renaud Métrich <rmetrich@redhat.com> Joe Nall <joe@nall.com> namoyer10 <48189779+namoyer10@users.noreply.github.com> Neiloy <neiloy@redhat.com> Axel Nennker <axel@nennker.de> Michele Newman <mnewman@redhat.com> nnerdmann <128606223+nnerdmann@users.noreply.github.com> Sean O'Keeffe <seanokeeffe797@gmail.com> Jiri Odehnal <jodehnal@redhat.com> Ilya Okomin <ilya.okomin@oracle.com> Kaustubh Padegaonkar <theTuxRacer@gmail.com> Michael Palmiotto <mpalmiotto@tresys.com> Eryx Paredes <eryxp@lyft.com> Max R.D. Parmer <maxp@trystero.is> Arnaud Patard <apatard@hupstream.com> Jan Pazdziora <jpazdziora@redhat.com> pcactr <paul.c.arnold4.ctr@mail.mil> Kenneth Peeples <kennethwpeeples@gmail.com> Nathan Peters <Nathaniel.Peters@ca.com> Frank Lin PIAT <fpiat@klabs.be> Stefan Pietsch <mail.ipv4v6+gh@gmail.com> piggyvenus <piggyvenus@gmail.com> Vojtech Polasek <vpolasek@redhat.com> Orion Poplawski <orion@nwra.com> Jennifer Power <barnabei.jennifer@gmail.com> Nick Poyant <npoyant@redhat.com> Martin Preisler <mpreisle@redhat.com> Wesley Ceraso Prudencio <wcerasop@redhat.com> Raphael Sanchez Prudencio <rsprudencio@redhat.com> Miha Purg <miha.purg@canonical.com> T.O. Radzy Radzykewycz <radzy@windriver.com> rain-Qing <yangyuqing6@qq.com> Kenyon Ralph <kenyon@kenyonralph.com> Mike Ralph <mralph@redhat.com> Federico Ramirez <federico.r.ramirez@oracle.com> rchikov <rumen.chikov@suse.com> Rick Renshaw <Richard_Renshaw@xtoenergy.com> Paul Rensing <prensing@cimetrics.com> Chris Reynolds <c.reynolds82@gmail.com> rhayes <rhayes@rivierautilities.com> Pat Riehecky <riehecky@fnal.gov> rlucente-se-jboss <rlucente@redhat.com> Juan Antonio Osorio Robles <juan.osoriorobles@eu.equinix.com> Paul Roche <paul.roche@menlosecurity.com> Jan Rodak <hony.com@seznam.cz> Matt Rogers <mrogers@redhat.com> Jesse Roland <jesse.roland@onyxpoint.com> Joshua Roys <roysjosh@gmail.com> rrenshaw <bofh69@yahoo.com> Daniel Ruf <daniel@daniel-ruf.de> Chris Ruffalo <chris.ruffalo@gmail.com> Benjamin Ruland <benjamin.ruland@gmail.com> rumch-se <77793453+rumch-se@users.noreply.github.com> Rutvik <rutksh@gmail.com> Ray Shaw (Cont ARL/CISD) rvshaw <rvshaw@esme.arl.army.mil> Nicolas SAID <nicolas.said@atos.net> Earl Sampson <ESampson@suse.com> sampsone <esampson@suse.com> Mirco Santori <mirco.santori@roche.com> Willy Santos <wsantos@redhat.com> Nagarjuna Sarvepalli <snagarju@redhat.com> Anderson Sasaki <33833274+ansasaki@users.noreply.github.com> Gautam Satish <gautams@hpe.com> Watson Sato <wsato@redhat.com> Satoru SATOH <satoru.satoh@gmail.com> Alexander Scheel <alexander.m.scheel@gmail.com> Bryan Schneiders <pschneiders@trisept.com> Robert Schweikert <rjschwei@suse.com> shaneboulden <shane.boulden@gmail.com> Vincent Shen <wenshen@redhat.com> Dhriti Shikhar <dhriti.shikhar.rokz@gmail.com> Spencer Shimko <sshimko@tresys.com> Mark Shoger <mshoger@redhat.com> Shane Siebken <shane.siebken@capellaspace.com> THOBY Simon <Simon.THOBY@viveris.fr> Thomas Sjögren <konstruktoid@users.noreply.github.com> Jindrich Skacel <102800748+jskacel@users.noreply.github.com> Alexandre Skrzyniarz <alexandre.skrzyniarz@laposte.net> Francisco Slavin <fslavin@tresys.com> sluetze <13255307+sluetze@users.noreply.github.com> Dave Smith <dsmith@eclipse.ncsc.mil> David Smith <dsmith@fornax.eclipse.ncsc.mil> Kevin Spargur <kspargur@redhat.com> Kenneth Stailey <kstailey.lists@gmail.com> Leland Steinke <leland.j.steinke.ctr@mail.mil> Justin Stephenson <jstephen@redhat.com> steven.y.gui <steven_ygui@163.com> Brian Stinson <brian@bstinson.com> Jake Stookey <jakestookey@gmail.com> Nathan Strahs <135379779+nathanstrahs@users.noreply.github.com> Jonathan Sturges <jsturges@redhat.com> svet-se <svetlin.boychev@suse.com> Kaushik Talathi <kaushik.talathi1@ibm.com> teacup-on-rockingchair <315160+teacup-on-rockingchair@users.noreply.github.com> Ian Tewksbury <itewk@redhat.com> Philippe Thierry <phil@reseau-libre.net> Simon THOBY <git@nightmared.fr> Derek Thurston <thegrit@gmail.com> tianzhenjia <jiatianzhen@cmss.chinamobile.com> Greg Tinsley <gtinsley@redhat.com> Paul Tittle <ptittle@cmf.nrl.navy.mil> tom <tom@localhost.localdomain> tomas.hudik <tomas.hudik@embedit.cz> Jeb Trayer <jeb.d.trayer@uscg.mil> TrilokGeer <tgeer@redhat.com> Viktors Trubovics <viktors.trubovics@suse.com> Nico Truzzolino <nico.truzzolino@gmx.de> Brian Turek <brian.turek@gmail.com> Matěj Týč <matyc@redhat.com> VadimDor <29509093+VadimDor@users.noreply.github.com> Trevor Vaughan <tvaughan@onyxpoint.com> vtrubovics <82443408+vtrubovics@users.noreply.github.com> Sophia Wang <huiwang@redhat.com> Samuel Warren <swarren@redhat.com> wcushen <54533890+wcushen@users.noreply.github.com> Shawn Wells <shawn@redhat.com> Whidix <31294015+Whidix@users.noreply.github.com> Daniel E. White <linuxdan@users.noreply.github.com> Bernhard M. Wiedemann <bwiedemann@suse.de> Roy Williams <roywilli@roywilli.redhat.com> Willumpie <willumpie@xs4all.nl> Rob Wilmoth <rwilmoth@redhat.com> win97pro <win97pro@protonmail.com> xcfxr <xucee@qq.com> Lucas Yamanishi <lucas.yamanishi@onyxpoint.com> Xirui Yang <xirui.yang@oracle.com> Yuqing Yang <yyq01323329@alibaba-inc.com> yarunachalam <yarunachalam@suse.com> Guang Yee <guang.yee@suse.com> Achilleas John Yfantis <ayfantis@redhat.com> YiLin.Li <YiLin.Li@linux.alibaba.com> yu410621 <lihuanyu410621@gmail.com> Xiaojie Yuan <xiyuan@redhat.com> yungcero <133906218+yungcero@users.noreply.github.com> yunimoo <yunimoo@nekocake.cafe> YuQing <yyq0391@163.com> zhaoyun <zhaoyun@kylinos.cn> Kevin Zimmerman <kevin.zimmerman@kitware.com> Luigi Mario Zuccarelli <luzuccar@redhat.com> Jan Černý <jcerny@redhat.com> Michal Šrubař <msrubar@redhat.com> https://github.com/ComplianceAsCode/content/releases/latest DRAFT - CIS Fedora Benchmark for Level 2 - Server This is a draft profile for experimental purposes. It is based on the CIS Fedora 40 Branch Benchmark maintained by CIS Community. Although the CIS Fedora Benchmarks are used as upstream for CIS Red Hat Benchmarks, they are not released as often as Fedora releases and therefore are expected to diverge along the time. This profile is currently maintained in best efforts for current Fedora releases. https://workbench.cisecurity.org/communities/101 DRAFT - CIS Fedora Benchmark for Level 1 - Server This is a draft profile for experimental purposes. It is based on the CIS Fedora 40 Branch Benchmark maintained by CIS Community. Although the CIS Fedora Benchmarks are used as upstream for CIS Red Hat Benchmarks, they are not released as often as Fedora releases and therefore are expected to diverge along the time. This profile is currently maintained in best efforts for current Fedora releases. https://workbench.cisecurity.org/communities/101 System Settings Contains rules that check correct system settings. Installing and Maintaining Software The following sections contain information on security-relevant choices during the initial operating system installation process and the setup of software updates. Prefer to use a 64-bit Operating System when supported Prefer installation of 64-bit operating systems when the CPU supports it. There is no remediation besides installing a 64-bit operating system. R1 Use of a 64-bit operating system offers a few advantages, like a larger address space range for Address Space Layout Randomization (ASLR) and systematic presence of No eXecute and Execute Disable (NX/XD) protection bits. System and Software Integrity System and software integrity can be gained by installing antivirus, increasing system encryption strength with FIPS, verifying installed software, enabling SELinux, installing an Intrusion Prevention System, etc. However, installing or enabling integrity checking tools cannot prevent intrusions, but they can detect that an intrusion may have occurred. Requirements for integrity checking may be highly dependent on the environment in which the system will be used. Snapshot-based approaches such as AIDE may induce considerable overhead in the presence of frequent software updates. Software Integrity Checking Both the AIDE (Advanced Intrusion Detection Environment) software and the RPM package management system provide mechanisms for verifying the integrity of installed software. AIDE uses snapshots of file metadata (such as hashes) and compares these to current system files in order to detect changes. The RPM package management system can conduct integrity checks by comparing information in its metadata database with files installed on the system. Verify Integrity with RPM The RPM package management system includes the ability to verify the integrity of installed packages by comparing the installed files with information about the files taken from the package metadata stored in the RPM database. Although an attacker could corrupt the RPM database (analogous to attacking the AIDE database as described above), this check can still reveal modification of important files. To list which files on the system differ from what is expected by the RPM database: $ rpm -qVa See the man page for rpm to see a complete explanation of each column. Verify File Hashes with RPM Without cryptographic integrity protections, system executables and files can be altered by unauthorized users without detection. The RPM package management system can check the hashes of installed software packages, including many that are important to system security. To verify that the cryptographic hash of system files and commands matches vendor values, run the following command to list which files on the system have hashes that differ from what is expected by the RPM database: $ rpm -Va --noconfig | grep '^..5' If the file was not expected to change, investigate the cause of the change using audit logs or other means. The package can then be reinstalled to restore the file. Run the following command to determine which package owns the file: $ rpm -qf FILENAME The package can be reinstalled from a dnf repository using the command: $ sudo dnf reinstall PACKAGENAME Alternatively, the package can be reinstalled from trusted media using the command: $ sudo rpm -Uvh PACKAGENAME This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of packages present on the system. It is not a problem in most cases, but especially systems with a large number of installed packages can be affected. 11 2 3 9 5.10.4.1 APO01.06 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS06.02 3.3.8 3.4.1 164.308(a)(1)(ii)(D) 164.312(b) 164.312(c)(1) 164.312(c)(2) 164.312(e)(2)(i) 4.3.4.3.2 4.3.4.3.3 4.3.4.4.4 SR 3.1 SR 3.3 SR 3.4 SR 3.8 SR 7.6 A.11.2.4 A.12.1.2 A.12.2.1 A.12.5.1 A.12.6.2 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 CM-6(d) CM-6(c) SI-7 SI-7(1) SI-7(6) AU-9(3) PR.DS-6 PR.DS-8 PR.IP-1 Req-11.5 SRG-OS-000480-GPOS-00227 11.5.2 The hashes of important files like system executables should match the information given by the RPM database. Executables with erroneous hashes could be a sign of nefarious activity on the system. # Remediation is applicable only in certain platforms if ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ); then # Find which files have incorrect hash (not in /etc, because of the system related config files) and then get files names files_with_incorrect_hash="$(rpm -Va --noconfig | grep -E '^..5' | awk '{print $NF}' )" if [ -n "$files_with_incorrect_hash" ]; then # From files names get package names and change newline to space, because rpm writes each package to new line packages_to_reinstall="$(rpm -qf $files_with_incorrect_hash | tr '\n' ' ')" dnf reinstall -y $packages_to_reinstall fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_hashes - name: 'Set fact: Package manager reinstall command' ansible.builtin.set_fact: package_manager_reinstall_cmd: dnf reinstall -y when: - not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) - ansible_distribution in [ "Fedora", "RedHat", "CentOS", "OracleLinux", "AlmaLinux" ] tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_hashes - name: 'Set fact: Package manager reinstall command (zypper)' ansible.builtin.set_fact: package_manager_reinstall_cmd: zypper in -f -y when: - not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) - ansible_distribution == "SLES" tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_hashes - name: Read files with incorrect hash ansible.builtin.command: rpm -Va --nodeps --nosize --nomtime --nordev --nocaps --nolinkto --nouser --nogroup --nomode --noghost --noconfig register: files_with_incorrect_hash changed_when: false failed_when: files_with_incorrect_hash.rc > 1 check_mode: false when: - not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) - (package_manager_reinstall_cmd is defined) tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_hashes - name: Create list of packages ansible.builtin.command: rpm -qf "{{ item }}" with_items: '{{ files_with_incorrect_hash.stdout_lines | map(''regex_findall'', ''^[.]+[5]+.* (\/.*)'', ''\1'') | map(''join'') | select(''match'', ''(\/.*)'') | list | unique }}' register: list_of_packages changed_when: false check_mode: false when: - not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) - files_with_incorrect_hash.stdout_lines is defined - (files_with_incorrect_hash.stdout_lines | length > 0) tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_hashes - name: Reinstall packages of files with incorrect hash ansible.builtin.command: '{{ package_manager_reinstall_cmd }} ''{{ item }}''' with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list | unique }}' when: - not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) - files_with_incorrect_hash.stdout_lines is defined - (package_manager_reinstall_cmd is defined and (files_with_incorrect_hash.stdout_lines | length > 0)) tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_hashes Verify and Correct File Permissions with RPM The RPM package management system can check file access permissions of installed software packages, including many that are important to system security. Verify that the file permissions of system files and commands match vendor values. Check the file permissions with the following command: $ sudo rpm -Va | awk '{ if (substr($0,2,1)=="M") print $NF }' Output indicates files that do not match vendor defaults. After locating a file with incorrect permissions, run the following command to determine which package owns it: $ rpm -qf FILENAME Next, run the following command to reset its permissions to the correct values: $ sudo rpm --restore PACKAGENAME Profiles may require that specific files have stricter file permissions than defined by the vendor. Such files will be reported as a finding and need to be evaluated according to your policy and deployment environment. This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of packages present on the system. It is not a problem in most cases, but especially systems with a large number of installed packages can be affected. 1 11 12 13 14 15 16 18 3 5 6 9 5.10.4.1 APO01.06 APO11.04 BAI03.05 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.04 DSS05.07 DSS06.02 MEA02.01 3.3.8 3.4.1 164.308(a)(1)(ii)(D) 164.312(b) 164.312(c)(1) 164.312(c)(2) 164.312(e)(2)(i) 4.3.3.3.9 4.3.3.5.8 4.3.3.7.3 4.3.4.3.2 4.3.4.3.3 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 5.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.5.1 A.12.6.2 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R4.2 CIP-003-8 R6 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CM-6(d) CM-6(c) SI-7 SI-7(1) SI-7(6) AU-9(3) CM-6(a) PR.AC-4 PR.DS-5 PR.IP-1 PR.PT-1 Req-11.5 SRG-OS-000256-GPOS-00097 SRG-OS-000257-GPOS-00098 SRG-OS-000258-GPOS-00099 SRG-OS-000278-GPOS-00108 11.5.2 Permissions on system binaries and configuration files that are too generous could allow an unauthorized user to gain privileges that they should not have. The permissions set by the vendor should be maintained. Any deviations from this baseline should be investigated. # Remediation is applicable only in certain platforms if ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ); then # Declare array to hold set of RPM packages we need to correct permissions for declare -A SETPERMS_RPM_DICT # Create a list of files on the system having permissions different from what # is expected by the RPM database readarray -t FILES_WITH_INCORRECT_PERMS < <(rpm -Va --nofiledigest | awk '{ if (substr($0,2,1)=="M") print $NF }') for FILE_PATH in "${FILES_WITH_INCORRECT_PERMS[@]}" do # NOTE: some files maybe controlled by more then one package readarray -t RPM_PACKAGES < <(rpm -qf "${FILE_PATH}") for RPM_PACKAGE in "${RPM_PACKAGES[@]}" do # Use an associative array to store packages as it's keys, not having to care about duplicates. SETPERMS_RPM_DICT["$RPM_PACKAGE"]=1 done done # For each of the RPM packages left in the list -- reset its permissions to the # correct values for RPM_PACKAGE in "${!SETPERMS_RPM_DICT[@]}" do rpm --restore "${RPM_PACKAGE}" done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_permissions - name: Read list of files with incorrect permissions ansible.builtin.command: rpm -Va --nodeps --nosignature --nofiledigest --nosize --nomtime --nordev --nocaps --nolinkto --nouser --nogroup register: files_with_incorrect_permissions failed_when: files_with_incorrect_permissions.rc > 1 changed_when: false check_mode: false when: not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_permissions - name: Create list of packages ansible.builtin.command: rpm -qf "{{ item }}" with_items: '{{ files_with_incorrect_permissions.stdout_lines | map(''regex_findall'', ''^[.]+[M]+.* (\/.*)'', ''\1'') | map(''join'') | select(''match'', ''(\/.*)'') | list | unique }}' register: list_of_packages changed_when: false check_mode: false when: - not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) - (files_with_incorrect_permissions.stdout_lines | length > 0) tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_permissions - name: Correct file permissions with RPM ansible.builtin.command: rpm --restore '{{ item }}' with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list | unique }}' when: - not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) - (files_with_incorrect_permissions.stdout_lines | length > 0) tags: - CJIS-5.10.4.1 - NIST-800-171-3.3.8 - NIST-800-171-3.4.1 - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(c) - NIST-800-53-CM-6(d) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - NIST-800-53-SI-7(6) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - high_complexity - high_severity - medium_disruption - no_reboot_needed - restrict_strategy - rpm_verify_permissions Verify Integrity with AIDE AIDE conducts integrity checks by comparing information about files with previously-gathered information. Ideally, the AIDE database is created immediately after initial system configuration, and then again after any software update. AIDE is highly configurable, with further configuration information located in /usr/share/doc/aide-VERSION . Install AIDE The aide package can be installed with the following command: $ sudo dnf install aide 1 11 12 13 14 15 16 2 3 5 7 8 9 5.10.1.3 APO01.06 BAI01.06 BAI02.01 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS04.07 DSS05.02 DSS05.03 DSS05.05 DSS05.07 DSS06.02 DSS06.06 4.3.4.3.2 4.3.4.3.3 4.3.4.4.4 SR 3.1 SR 3.3 SR 3.4 SR 3.8 SR 4.1 SR 6.2 SR 7.6 1034 1288 1341 1417 A.11.2.4 A.12.1.2 A.12.2.1 A.12.4.1 A.12.5.1 A.12.6.2 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.14.2.7 A.15.2.1 A.8.2.3 CM-6(a) DE.CM-1 DE.CM-7 PR.DS-1 PR.DS-6 PR.DS-8 PR.IP-1 PR.IP-3 Req-11.5 SRG-OS-000445-GPOS-00199 R76 R79 6.1.1 11.5.2 The AIDE package must be installed if it is to be available for integrity checking. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "aide" ; then dnf install -y "aide" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_aide_installed - name: Ensure aide is installed ansible.builtin.package: name: aide state: present when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_aide_installed include install_aide class install_aide { package { 'aide': ensure => 'installed', } } package --add=aide [[packages]] name = "aide" version = "*" package install aide dnf install aide Build and Test AIDE Database Run the following command to generate a new database: $ sudo /usr/sbin/aide --init By default, the database will be written to the file /var/lib/aide/aide.db.new.gz. Storing the database, the configuration file /etc/aide.conf, and the binary /usr/sbin/aide (or hashes of these files), in a secure location (such as on read-only media) provides additional assurance about their integrity. The newly-generated database can be installed as follows: $ sudo cp /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz To initiate a manual check, run the following command: $ sudo /usr/sbin/aide --check If this check produces any unexpected output, investigate. 1 11 12 13 14 15 16 2 3 5 7 8 9 5.10.1.3 APO01.06 BAI01.06 BAI02.01 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS04.07 DSS05.02 DSS05.03 DSS05.05 DSS05.07 DSS06.02 DSS06.06 4.3.4.3.2 4.3.4.3.3 4.3.4.4.4 SR 3.1 SR 3.3 SR 3.4 SR 3.8 SR 4.1 SR 6.2 SR 7.6 A.11.2.4 A.12.1.2 A.12.2.1 A.12.4.1 A.12.5.1 A.12.6.2 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.14.2.7 A.15.2.1 A.8.2.3 CM-6(a) DE.CM-1 DE.CM-7 PR.DS-1 PR.DS-6 PR.DS-8 PR.IP-1 PR.IP-3 Req-11.5 SRG-OS-000445-GPOS-00199 R76 R79 6.1.1 11.5.2 For AIDE to be effective, an initial database of "known-good" information about files must be captured and it should be able to be verified against the installed files. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "aide" ; then dnf install -y "aide" fi /usr/sbin/aide --init /bin/cp -p /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_build_database - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Build and Test AIDE Database - Ensure AIDE Is Installed ansible.builtin.package: name: '{{ item }}' state: present with_items: - aide when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_build_database - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Build and Test AIDE Database - Build and Test AIDE Database ansible.builtin.command: /usr/sbin/aide --init changed_when: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_build_database - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Build and Test AIDE Database - Check Whether the Stock AIDE Database Exists ansible.builtin.stat: path: /var/lib/aide/aide.db.new.gz register: aide_database_stat when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_build_database - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Build and Test AIDE Database - Stage AIDE Database ansible.builtin.copy: src: /var/lib/aide/aide.db.new.gz dest: /var/lib/aide/aide.db.gz backup: true remote_src: true when: - '"kernel" in ansible_facts.packages' - (aide_database_stat.stat.exists is defined and aide_database_stat.stat.exists) tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_build_database - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Configure AIDE to Verify the Audit Tools The operating system file integrity tool must be configured to protect the integrity of the audit tools. AU-9(3) AU-9(3).1 SRG-OS-000278-GPOS-00108 6.1.3 Protecting the integrity of the tools used for auditing purposes is a critical step toward ensuring the integrity of audit information. Audit information includes all information (e.g., audit records, audit settings, and audit reports) needed to successfully audit information system activity. Audit tools include but are not limited to vendor-provided and open-source audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators. It is not uncommon for attackers to replace the audit tools or inject code into the existing tools to provide the capability to hide or erase system activity from the audit logs. To address this risk, audit tools must be cryptographically signed to provide the capability to identify when the audit tools have been modified, manipulated, or replaced. An example is a checksum hash of the file or files. Configure Periodic Execution of AIDE At a minimum, AIDE should be configured to run a weekly scan. To implement a daily execution of AIDE at 4:05am using cron, add the following line to /etc/crontab: 05 4 * * * root /usr/sbin/aide --check To implement a weekly execution of AIDE at 4:05am using cron, add the following line to /etc/crontab: 05 4 * * 0 root /usr/sbin/aide --check AIDE can be executed periodically through other means; this is merely one example. The usage of cron's special time codes, such as @daily and @weekly is acceptable. 1 11 12 13 14 15 16 2 3 5 7 8 9 5.10.1.3 APO01.06 BAI01.06 BAI02.01 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS04.07 DSS05.02 DSS05.03 DSS05.05 DSS05.07 DSS06.02 DSS06.06 4.3.4.3.2 4.3.4.3.3 4.3.4.4.4 SR 3.1 SR 3.3 SR 3.4 SR 3.8 SR 4.1 SR 6.2 SR 7.6 A.11.2.4 A.12.1.2 A.12.2.1 A.12.4.1 A.12.5.1 A.12.6.2 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.14.2.7 A.15.2.1 A.8.2.3 SI-7 SI-7(1) CM-6(a) DE.CM-1 DE.CM-7 PR.DS-1 PR.DS-6 PR.DS-8 PR.IP-1 PR.IP-3 Req-11.5 SRG-OS-000363-GPOS-00150 SRG-OS-000446-GPOS-00200 SRG-OS-000447-GPOS-00201 R76 6.1.2 11.5.2 By default, AIDE does not install itself for periodic execution. Periodically running AIDE is necessary to reveal unexpected changes in installed files. Unauthorized changes to the baseline configuration could make the system vulnerable to various attacks or allow unauthorized access to the operating system. Changes to operating system configurations can have unintended side effects, some of which may be relevant to security. Detecting such changes and providing an automated response can help avoid unintended, negative consequences that could ultimately affect the security state of the operating system. The operating system's Information Management Officer (IMO)/Information System Security Officer (ISSO) and System Administrators (SAs) must be notified via email and/or monitoring system trap when there is an unauthorized modification of a configuration item. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "aide" ; then dnf install -y "aide" fi if ! grep -q "/usr/sbin/aide --check" /etc/crontab ; then echo "05 4 * * * root /usr/sbin/aide --check" >> /etc/crontab else sed -i '\!^.* --check.*$!d' /etc/crontab echo "05 4 * * * root /usr/sbin/aide --check" >> /etc/crontab fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_periodic_cron_checking - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure AIDE is installed ansible.builtin.package: name: aide state: present when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_periodic_cron_checking - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Install cron ansible.builtin.package: name: cronie state: present when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_periodic_cron_checking - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Periodic Execution of AIDE ansible.builtin.cron: name: run AIDE check minute: 5 hour: 4 weekday: 0 user: root job: /usr/sbin/aide --check when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.3 - NIST-800-53-CM-6(a) - NIST-800-53-SI-7 - NIST-800-53-SI-7(1) - PCI-DSS-Req-11.5 - PCI-DSSv4-11.5.2 - aide_periodic_cron_checking - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Federal Information Processing Standard (FIPS) The Federal Information Processing Standard (FIPS) is a computer security standard which is developed by the U.S. Government and industry working groups to validate the quality of cryptographic modules. The FIPS standard provides four security levels to ensure adequate coverage of different industries, implementation of cryptographic modules, and organizational sizes and requirements. FIPS 140-2 is the current standard for validating that mechanisms used to access cryptographic modules utilize authentication that meets industry and government requirements. For government systems, this allows Security Levels 1, 2, 3, or 4 for use on Fedora. See http://csrc.nist.gov/publications/PubsFIPS.html for more information. Enable Dracut FIPS Module To enable FIPS mode, run the following command: fips-mode-setup --enable To enable FIPS, the system requires that the fips module is added in dracut configuration. Check if /etc/dracut.conf.d/40-fips.conf contain add_dracutmodules+=" fips " The system needs to be rebooted for these changes to take effect. System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process. 1446 CIP-003-8 R4.2 CIP-007-3 R5.1 SC-12(2) SC-12(3) IA-7 SC-13 CM-6(a) SC-12 FCS_RBG_EXT.1 SRG-OS-000478-GPOS-00223 Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data. The operating system must implement cryptographic modules adhering to the higher standards approved by the federal government since this provides assurance they have been tested and validated. Enable FIPS Mode To enable FIPS mode, run the following command: fips-mode-setup --enable The fips-mode-setup command will configure the system in FIPS mode by automatically configuring the following: Setting the kernel FIPS mode flag (/proc/sys/crypto/fips_enabled) to 1 Creating /etc/system-fips Setting the system crypto policy in /etc/crypto-policies/config to Loading the Dracut fips module To configure Fedora to run in FIPS 140 mode, the kernel parameter "fips=1" needs to be added during its installation. Only enabling FIPS 140 mode during the Fedora installation ensures that the system generates all keys with FIPS-approved algorithms and continuous monitoring tests in place. Enabling FIPS mode on a preexisting system involves a number of modifications to it and therefore is not supported. This rule DOES NOT CHECK if the components of the operating system are FIPS certified. You can find the list of FIPS certified modules at https://csrc.nist.gov/projects/cryptographic-module-validation-program/validated-modules/search. This rule checks if the system is running in FIPS mode. 1446 CIP-003-8 R4.2 CIP-007-3 R5.1 CM-3(6) SC-12(2) SC-12(3) IA-7 SC-13 CM-6(a) SC-12 FCS_COP.1(1) FCS_COP.1(2) FCS_COP.1(3) FCS_COP.1(4) FCS_CKM.1 FCS_CKM.2 FCS_TLSC_EXT.1 FCS_RBG_EXT.1 SRG-OS-000478-GPOS-00223 SRG-OS-000396-GPOS-00176 Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data. The operating system must implement cryptographic modules adhering to the higher standards approved by the federal government since this provides assurance they have been tested and validated. # Remediation is applicable only in certain platforms if ( ! ( [ "${container:-}" == "bwrap-osbuild" ] ) && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; }; then cat > /usr/lib/bootc/kargs.d/01-fips.toml << EOF kargs = ["fips=1"] EOF fi else >&2 echo 'Remediation is not applicable, nothing was done' fi [customizations] fips = true Ensure '/etc/system-fips' exists On a system where FIPS mode is enabled, /etc/system-fips must exist. To enable FIPS mode, run the following command: fips-mode-setup --enable The system needs to be rebooted for these changes to take effect. System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process. CIP-003-8 R4.2 CIP-007-3 R5.1 SC-12(2) SC-12(3) IA-7 SC-13 CM-6(a) SC-12 Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data. The operating system must implement cryptographic modules adhering to the higher standards approved by the federal government since this provides assurance they have been tested and validated. Set kernel parameter 'crypto.fips_enabled' to 1 System running in FIPS mode is indicated by kernel parameter 'crypto.fips_enabled'. This parameter should be set to 1 in FIPS mode. To enable FIPS mode, run the following command: fips-mode-setup --enable To enable strict FIPS compliance, the fips=1 kernel option needs to be added to the kernel boot parameters during system installation so key generation is done with FIPS-approved algorithms and continuous monitoring tests in place. The system needs to be rebooted for these changes to take effect. System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process. CIP-003-8 R4.2 CIP-007-3 R5.1 SC-12(2) SC-12(3) IA-7 SC-13 CM-6(a) SC-12 SRG-OS-000033-GPOS-00014 SRG-OS-000125-GPOS-00065 SRG-OS-000250-GPOS-00093 SRG-OS-000393-GPOS-00173 SRG-OS-000394-GPOS-00174 SRG-OS-000396-GPOS-00176 SRG-OS-000423-GPOS-00187 SRG-OS-000478-GPOS-00223 Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data. The operating system must implement cryptographic modules adhering to the higher standards approved by the federal government since this provides assurance they have been tested and validated. System Cryptographic Policies Linux has the capability to centrally configure cryptographic polices. The command update-crypto-policies is used to set the policy applicable for the various cryptographic back-ends, such as SSL/TLS libraries. The configured cryptographic policies will be the default policy used by these backends unless the application user configures them otherwise. When the system has been configured to use the centralized cryptographic policies, the administrator is assured that any application that utilizes the supported backends will follow a policy that adheres to the configured profile. Currently the supported backends are: GnuTLS libraryOpenSSL libraryNSS libraryOpenJDKLibkrb5BINDOpenSSH Applications and languages which rely on any of these backends will follow the system policies as well. Examples are apache httpd, nginx, php, and others. The system-provided crypto policies Specify the crypto policy for the system. DEFAULT DEFAULT DEFAULT:NO-SHA1 FIPS FIPS:OSPP FIPS:STIG LEGACY FUTURE NEXT Configure BIND to use System Crypto Policy Crypto Policies provide a centralized control over crypto algorithms usage of many packages. BIND is supported by crypto policy, but the BIND configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the /etc/named.conf includes the appropriate configuration: In the options section of /etc/named.conf, make sure that the following line is not commented out or superseded by later includes: include "/etc/crypto-policies/back-ends/bind.config"; CIP-003-8 R4.2 CIP-007-3 R5.1 SC-13 SC-12(2) SC-12(3) SRG-OS-000423-GPOS-00187 SRG-OS-000426-GPOS-00190 Overriding the system crypto policy makes the behavior of the BIND service violate expectations, and makes system configuration more fragmented. # Remediation is applicable only in certain platforms if rpm --quiet -q bind; then function remediate_bind_crypto_policy() { CONFIG_FILE="/etc/named.conf" if test -f "$CONFIG_FILE"; then sed -i 's|options {|&\n\tinclude "/etc/crypto-policies/back-ends/bind.config";|' "$CONFIG_FILE" return 0 else echo "Aborting remediation as '$CONFIG_FILE' was not even found." >&2 return 1 fi } remediate_bind_crypto_policy else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - configure_bind_crypto_policy - configure_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - name: Configure BIND to use System Crypto Policy - Check BIND configuration file exists ansible.builtin.stat: path: /etc/named.conf register: bind_config_file when: '"bind" in ansible_facts.packages' tags: - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - configure_bind_crypto_policy - configure_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - name: Configure BIND to use System Crypto Policy - Aborting remediation, file not found ansible.builtin.debug: msg: Aborting remediation as '/etc/named.conf' was not found. when: - '"bind" in ansible_facts.packages' - not bind_config_file.stat.exists tags: - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - configure_bind_crypto_policy - configure_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - name: Configure BIND to use System Crypto Policy - Insert crypto-policy into BIND config ansible.builtin.lineinfile: path: /etc/named.conf insertafter: ^\s*options\s*{ line: ' include "/etc/crypto-policies/back-ends/bind.config";' state: present when: - '"bind" in ansible_facts.packages' - bind_config_file.stat.exists tags: - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - configure_bind_crypto_policy - configure_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed Configure System Cryptography Policy To configure the system cryptography policy to use ciphers only from the policy, run the following command: $ sudo update-crypto-policies --set The rule checks if settings for selected crypto policy are configured as expected. Configuration files in the /etc/crypto-policies/back-ends are either symlinks to correct files provided by Crypto-policies package or they are regular files in case crypto policy customizations are applied. Crypto policies may be customized by crypto policy modules, in which case it is delimited from the base policy using a colon. The system needs to be rebooted for these changes to take effect. System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process. 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.312(e)(1) 164.312(e)(2)(ii) 1446 CIP-003-8 R4.2 CIP-007-3 R5.1 CIP-007-3 R7.1 AC-17(a) AC-17(2) CM-6(a) MA-4(6) SC-13 SC-12(2) SC-12(3) FCS_COP.1(1) FCS_COP.1(2) FCS_COP.1(3) FCS_COP.1(4) FCS_CKM.1 FCS_CKM.2 FCS_TLSC_EXT.1 SRG-OS-000396-GPOS-00176 SRG-OS-000393-GPOS-00173 SRG-OS-000394-GPOS-00174 1.6.1 2.2.7 2.2 Centralized cryptographic policies simplify applying secure ciphers across an operating system and the applications that run on that operating system. Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data. var_system_crypto_policy='' stderr_of_call=$(update-crypto-policies --set ${var_system_crypto_policy} 2>&1 > /dev/null) rc=$? if test "$rc" = 127; then echo "$stderr_of_call" >&2 echo "Make sure that the script is installed on the remediated system." >&2 echo "See output of the 'dnf provides update-crypto-policies' command" >&2 echo "to see what package to (re)install" >&2 false # end with an error code elif test "$rc" != 0; then echo "Error invoking the update-crypto-policies script: $stderr_of_call" >&2 false # end with an error code fi - name: XCCDF Value var_system_crypto_policy # promote to variable set_fact: var_system_crypto_policy: !!str tags: - always - name: Configure System Cryptography Policy ansible.builtin.lineinfile: path: /etc/crypto-policies/config regexp: ^(?!#)(\S+)$ line: '{{ var_system_crypto_policy }}' create: true tags: - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.7 - configure_crypto_policy - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - name: Verify that Crypto Policy is Set (runtime) ansible.builtin.command: /usr/bin/update-crypto-policies --set {{ var_system_crypto_policy }} tags: - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.7 - configure_crypto_policy - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: configure-crypto-policy.service enabled: true contents: | [Unit] Before=kubelet.service [Service] Type=oneshot ExecStart=update-crypto-policies --set {{.var_system_crypto_policy}} RemainAfterExit=yes [Install] WantedBy=multi-user.target Configure GnuTLS library to use DoD-approved TLS Encryption Crypto Policies provide a centralized control over crypto algorithms usage of many packages. GnuTLS is supported by system crypto policy, but the GnuTLS configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that /etc/crypto-policies/back-ends/gnutls.config contains the following line and is not commented out: +VERS-ALL:-VERS-DTLS0.9:-VERS-TLS1.1:-VERS-TLS1.0:-VERS-SSL3.0:-VERS-DTLS1.0 These keywords are order-independent, so the line can be in any order. GnuTLS will then prefer the highest version. AC-17(2) SRG-OS-000250-GPOS-00093 SRG-OS-000423-GPOS-00187 Overriding the system crypto policy makes the behavior of the GnuTLS library violate expectations, and makes system configuration more fragmented. CONF_FILE=/etc/crypto-policies/back-ends/gnutls.config correct_value='+VERS-ALL:-VERS-DTLS0.9:-VERS-TLS1.1:-VERS-TLS1.0:-VERS-SSL3.0:-VERS-DTLS1.0' grep -q ${correct_value} ${CONF_FILE} if [[ $? -ne 0 ]]; then # We need to get the existing value, using PCRE to maintain same regex existing_value=$(grep -Po '(\+VERS-ALL(?::-VERS-[A-Z]+\d\.\d)+)' ${CONF_FILE}) if [[ ! -z ${existing_value} ]]; then # replace existing_value with correct_value sed -i "s/${existing_value}/${correct_value}/g" ${CONF_FILE} else # ***NOTE*** # # This probably means this file is not here or it's been modified # unintentionally. # ********** # # echo correct_value to end echo ${correct_value} >> ${CONF_FILE} fi fi - name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: set_fact' ansible.builtin.set_fact: path: /etc/crypto-policies/back-ends/gnutls.config correct_value: +VERS-ALL:-VERS-DTLS0.9:-VERS-TLS1.1:-VERS-TLS1.0:-VERS-SSL3.0:-VERS-DTLS1.0 lineinfile_reg: \+VERS-ALL:-VERS-DTLS0\.9:-VERS-TLS1\.1:-VERS-TLS1\.0:-VERS-SSL3\.0:-VERS-DTLS1\.0 tags: - NIST-800-53-AC-17(2) - configure_gnutls_tls_crypto_policy - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: stat' ansible.builtin.stat: path: '{{ path }}' follow: true register: gnutls_file tags: - NIST-800-53-AC-17(2) - configure_gnutls_tls_crypto_policy - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: Add' ansible.builtin.lineinfile: path: '{{ path }}' regexp: '{{ lineinfile_reg }}' line: '{{ correct_value }}' create: true when: not gnutls_file.stat.exists or gnutls_file.stat.size <= correct_value|length tags: - NIST-800-53-AC-17(2) - configure_gnutls_tls_crypto_policy - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Configure GnuTLS library to use DoD-approved TLS Encryption block: - name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: Existing value check' ansible.builtin.lineinfile: path: '{{ path }}' create: false regexp: '{{ lineinfile_reg }}' state: absent check_mode: true changed_when: false register: gnutls - name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: Update' ansible.builtin.replace: path: '{{ path }}' regexp: (\+VERS-ALL(?::-VERS-[A-Z]+\d\.\d)+) replace: '{{ correct_value }}' when: gnutls.found is defined and gnutls.found != 1 when: gnutls_file.stat.exists and gnutls_file.stat.size > correct_value|length tags: - NIST-800-53-AC-17(2) - configure_gnutls_tls_crypto_policy - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Configure Kerberos to use System Crypto Policy Crypto Policies provide a centralized control over crypto algorithms usage of many packages. Kerberos is supported by crypto policy, but it's configuration may be set up to ignore it. To check that Crypto Policies settings for Kerberos are configured correctly, examine that there is a symlink at /etc/krb5.conf.d/crypto-policies targeting /etc/cypto-policies/back-ends/krb5.config. If the symlink exists, Kerberos is configured to use the system-wide crypto policy settings. 0418 1055 1402 CIP-003-8 R4.2 CIP-007-3 R5.1 SC-13 SC-12(2) SC-12(3) SRG-OS-000120-GPOS-00061 Overriding the system crypto policy makes the behavior of Kerberos violate expectations, and makes system configuration more fragmented. rm -f /etc/krb5.conf.d/crypto-policies ln -s /etc/crypto-policies/back-ends/krb5.config /etc/krb5.conf.d/crypto-policies - name: Configure Kerberos to use System Crypto Policy ansible.builtin.file: src: /etc/crypto-policies/back-ends/krb5.config path: /etc/krb5.conf.d/crypto-policies state: link tags: - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - configure_kerberos_crypto_policy - configure_strategy - high_severity - low_complexity - low_disruption - reboot_required Configure Libreswan to use System Crypto Policy Crypto Policies provide a centralized control over crypto algorithms usage of many packages. Libreswan is supported by system crypto policy, but the Libreswan configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the /etc/ipsec.conf includes the appropriate configuration file. In /etc/ipsec.conf, make sure that the following line is not commented out or superseded by later includes: include /etc/crypto-policies/back-ends/libreswan.config CIP-003-8 R4.2 CIP-007-3 R5.1 CM-6(a) MA-4(6) SC-13 SC-12(2) SC-12(3) Req-2.2 SRG-OS-000033-GPOS-00014 Overriding the system crypto policy makes the behavior of the Libreswan service violate expectations, and makes system configuration more fragmented. function remediate_libreswan_crypto_policy() { CONFIG_FILE="/etc/ipsec.conf" if ! grep -qP "^\s*include\s+/etc/crypto-policies/back-ends/libreswan.config\s*(?:#.*)?$" "$CONFIG_FILE" ; then # the file might not end with a new line echo -e '\ninclude /etc/crypto-policies/back-ends/libreswan.config' >> "$CONFIG_FILE" fi return 0 } remediate_libreswan_crypto_policy - name: Configure Libreswan to use System Crypto Policy ansible.builtin.lineinfile: path: /etc/ipsec.conf line: include /etc/crypto-policies/back-ends/libreswan.config create: true tags: - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - PCI-DSS-Req-2.2 - configure_libreswan_crypto_policy - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy Configure OpenSSL library to use System Crypto Policy Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSL is supported by crypto policy, but the OpenSSL configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, you have to examine the OpenSSL config file available under /etc/pki/tls/openssl.cnf. This file has the ini format, and it enables crypto policy support if there is a [ crypto_policy ] section that contains the .include = /etc/crypto-policies/back-ends/opensslcnf.config directive. CIP-003-8 R4.2 CIP-007-3 R5.1 CIP-007-3 R7.1 AC-17(a) AC-17(2) CM-6(a) MA-4(6) SC-13 SC-12(2) SC-12(3) FCS_CKM.1 FCS_CKM.1.1 FCS_CKM.2 FCS_COP.1/ENCRYPT FCS_COP.1/HASH FCS_COP.1/SIGN FCS_COP.1/KEYHMAC FCS_TLSC_EXT.1 FCS_TLSC_EXT.1.1 Req-2.2 SRG-OS-000250-GPOS-00093 Overriding the system crypto policy makes the behavior of the Java runtime violates expectations, and makes system configuration more fragmented. OPENSSL_CRYPTO_POLICY_SECTION='[ crypto_policy ]' OPENSSL_CRYPTO_POLICY_SECTION_REGEX='\[\s*crypto_policy\s*\]' OPENSSL_CRYPTO_POLICY_INCLUSION='.include = /etc/crypto-policies/back-ends/opensslcnf.config' OPENSSL_CRYPTO_POLICY_INCLUSION_REGEX='^\s*\.include\s*(?:=\s*)?/etc/crypto-policies/back-ends/opensslcnf.config$' function remediate_openssl_crypto_policy() { CONFIG_FILE=/etc/pki/tls/openssl.cnf if test -f "$CONFIG_FILE"; then if ! grep -q "^\\s*$OPENSSL_CRYPTO_POLICY_SECTION_REGEX" "$CONFIG_FILE"; then printf '\n%s\n\n%s' "$OPENSSL_CRYPTO_POLICY_SECTION" "$OPENSSL_CRYPTO_POLICY_INCLUSION" >> "$CONFIG_FILE" return 0 elif ! grep -q "^\\s*$OPENSSL_CRYPTO_POLICY_INCLUSION_REGEX" "$CONFIG_FILE"; then sed -i "s|$OPENSSL_CRYPTO_POLICY_SECTION_REGEX|&\\n\\n$OPENSSL_CRYPTO_POLICY_INCLUSION\\n|" "$CONFIG_FILE" return 0 fi else echo "Aborting remediation as '$CONFIG_FILE' was not even found." >&2 return 1 fi } remediate_openssl_crypto_policy - name: Configure OpenSSL library to use System Crypto Policy - Search for crypto_policy Section ansible.builtin.find: paths: /etc/pki/tls patterns: openssl.cnf contains: ^\s*\[\s*crypto_policy\s*] register: test_crypto_policy_group tags: - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - PCI-DSS-Req-2.2 - configure_openssl_crypto_policy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Configure OpenSSL library to use System Crypto Policy - Search for crypto_policy Section Together With .include Directive ansible.builtin.find: paths: /etc/pki/tls patterns: openssl.cnf contains: ^\s*\.include\s*(?:=\s*)?/etc/crypto-policies/back-ends/opensslcnf.config$ register: test_crypto_policy_include_directive tags: - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - PCI-DSS-Req-2.2 - configure_openssl_crypto_policy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Configure OpenSSL library to use System Crypto Policy - Add .include Line for opensslcnf.config File in crypto_policy Section ansible.builtin.lineinfile: create: true insertafter: ^\s*\[\s*crypto_policy\s*]\s* line: .include = /etc/crypto-policies/back-ends/opensslcnf.config path: /etc/pki/tls/openssl.cnf when: - test_crypto_policy_group.matched > 0 - test_crypto_policy_include_directive.matched == 0 tags: - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - PCI-DSS-Req-2.2 - configure_openssl_crypto_policy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Configure OpenSSL library to use System Crypto Policy - Add crypto_policy Section With .include for opensslcnf.config File ansible.builtin.lineinfile: create: true line: |- [crypto_policy] .include = /etc/crypto-policies/back-ends/opensslcnf.config path: /etc/pki/tls/openssl.cnf when: test_crypto_policy_group.matched == 0 tags: - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-12(2) - NIST-800-53-SC-12(3) - NIST-800-53-SC-13 - PCI-DSS-Req-2.2 - configure_openssl_crypto_policy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Configure SSH to use System Crypto Policy Crypto Policies provide a centralized control over crypto algorithms usage of many packages. SSH is supported by crypto policy, but the SSH configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the CRYPTO_POLICY variable is either commented or not set at all in the /etc/sysconfig/sshd. 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.312(e)(1) 164.312(e)(2)(ii) CIP-003-8 R4.2 CIP-007-3 R5.1 CIP-007-3 R7.1 AC-17(a) AC-17(2) CM-6(a) MA-4(6) SC-13 FCS_SSH_EXT.1 FCS_SSHS_EXT.1 FCS_SSHC_EXT.1 Req-2.2 SRG-OS-000250-GPOS-00093 2.2.7 2.2 Overriding the system crypto policy makes the behavior of the SSH service violate expectations, and makes system configuration more fragmented. SSH_CONF="/etc/sysconfig/sshd" sed -i "/^\s*CRYPTO_POLICY.*$/Id" $SSH_CONF - name: Configure SSH to use System Crypto Policy ansible.builtin.lineinfile: dest: /etc/sysconfig/sshd state: absent regexp: (?i)^\s*CRYPTO_POLICY.*$ tags: - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(6) - NIST-800-53-SC-13 - PCI-DSS-Req-2.2 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.7 - configure_ssh_crypto_policy - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required Harden SSH client Crypto Policy Crypto Policies are means of enforcing certain cryptographic settings for selected applications including OpenSSH client. To override the system wide crypto policy for Openssh client, place a file in the /etc/ssh/ssh_config.d/ so that it is loaded before the 05-redhat.conf. In this case it is file named 02-ospp.conf containing parameters which need to be changed with respect to the crypto policy. This rule checks if the file exists and if it contains required parameters and values which modify the Crypto Policy. During the parsing process, as soon as Openssh client parses some configuration option and its value, it remembers it and ignores any subsequent overrides. The customization mechanism provided by crypto policies appends eventual customizations at the end of the system wide crypto policy. Therefore, if the crypto policy customization overrides some parameter which is already configured in the system wide crypto policy, the SSH client will not honor that customized parameter. CIP-003-8 R4.2 CIP-007-3 R5.1 CIP-007-3 R7.1 AC-17(a) AC-17(2) CM-6(a) MA-4(6) SC-13 SRG-OS-000033-GPOS-00014 SRG-OS-000250-GPOS-00093 SRG-OS-000393-GPOS-00173 SRG-OS-000394-GPOS-00174 The Common Criteria requirements specify how certain parameters for OpenSSH Client are configured. Particular parameters are RekeyLimit, GSSAPIAuthentication, Ciphers, PubkeyAcceptedKeyTypes, MACs and KexAlgorithms. Currently particular requirements specified by CC are stricter compared to any existing Crypto Policy. #the file starts with 02 so that it is loaded before the 05-redhat.conf which activates configuration provided by system vide crypto policy file="/etc/ssh/ssh_config.d/02-ospp.conf" echo -e "Match final all\n\ RekeyLimit 512M 1h\n\ GSSAPIAuthentication no\n\ Ciphers aes256-ctr,aes256-cbc,aes128-ctr,aes128-cbc\n\ PubkeyAcceptedKeyTypes ssh-rsa,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256\n\ MACs hmac-sha2-512,hmac-sha2-256\n\ KexAlgorithms ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group14-sha1\n" > "$file" Configure SSH Client to Use FIPS 140 Validated Ciphers: openssh.config Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSH is supported by system crypto policy, but the OpenSSH configuration may be set up incorrectly. To check that Crypto Policies settings for ciphers are configured correctly, ensure that /etc/crypto-policies/back-ends/openssh.config contains the following line and is not commented out: Ciphers The system needs to be rebooted for these changes to take effect. System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process. AC-17(2) SRG-OS-000033-GPOS-00014 SRG-OS-000125-GPOS-00065 SRG-OS-000250-GPOS-00093 SRG-OS-000393-GPOS-00173 SRG-OS-000394-GPOS-00174 SRG-OS-000423-GPOS-00187 Overriding the system crypto policy makes the behavior of the OpenSSH client violate expectations, and makes system configuration more fragmented. By specifying a cipher list with the order of ciphers being in a “strongest to weakest” orientation, the system will automatically attempt to use the strongest cipher for securing SSH connections. sshd_approved_ciphers='' if [ -e "/etc/crypto-policies/back-ends/openssh.config" ] ; then LC_ALL=C sed -i "/^.*Ciphers\s\+/d" "/etc/crypto-policies/back-ends/openssh.config" else touch "/etc/crypto-policies/back-ends/openssh.config" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/crypto-policies/back-ends/openssh.config" cp "/etc/crypto-policies/back-ends/openssh.config" "/etc/crypto-policies/back-ends/openssh.config.bak" # Insert at the end of the file printf '%s\n' "Ciphers ${sshd_approved_ciphers}" >> "/etc/crypto-policies/back-ends/openssh.config" # Clean up after ourselves. rm "/etc/crypto-policies/back-ends/openssh.config.bak" - name: XCCDF Value sshd_approved_ciphers # promote to variable set_fact: sshd_approved_ciphers: !!str tags: - always - name: 'Configure SSH Daemon to Use FIPS 140-2 Validated Ciphers: openssh.config' block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/crypto-policies/back-ends/openssh.config create: true regexp: (?i)^.*Ciphers\s+ state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/crypto-policies/back-ends/openssh.config ansible.builtin.lineinfile: path: /etc/crypto-policies/back-ends/openssh.config create: true regexp: (?i)^.*Ciphers\s+ state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/crypto-policies/back-ends/openssh.config ansible.builtin.lineinfile: path: /etc/crypto-policies/back-ends/openssh.config create: true regexp: (?i)^.*Ciphers\s+ line: Ciphers {{ sshd_approved_ciphers }} state: present tags: - NIST-800-53-AC-17(2) - harden_sshd_ciphers_openssh_conf_crypto_policy - high_severity - low_complexity - low_disruption - reboot_required - restrict_strategy Harden SSHD Crypto Policy Crypto Policies are means of enforcing certain cryptographic settings for selected applications including OpenSSH server. The SSHD service is by default configured to modify its configuration based on currently configured Crypto-Policy. However, in certain cases it might be needed to override the Crypto Policy specific to OpenSSH Server and leave rest of the Crypto Policy intact. This can be done by dropping a file named opensshserver-xxx.config, replacing xxx with arbitrary identifier, into /etc/crypto-policies/local.d. This has to be followed by running update-crypto-policies so that changes are applied. Changes are propagated into /etc/crypto-policies/back-ends/opensshserver.config. This rule checks if this file contains predefined CRYPTO_POLICY environment variable configured with predefined value. CIP-003-8 R4.2 CIP-007-3 R5.1 CIP-007-3 R7.1 AC-17(a) AC-17(2) CM-6(a) MA-4(6) SC-13 SC-12(2) SC-12(3) SRG-OS-000250-GPOS-00093 SRG-OS-000033-GPOS-00014 SRG-OS-000120-GPOS-00061 The Common Criteria requirements specify that certain parameters for OpenSSH Server are configured e.g. supported ciphers, accepted host key algorithms, public key types, key exchange algorithms, HMACs and GSSAPI key exchange is disabled. Currently particular requirements specified by CC are stricter compared to any existing Crypto Policy. # Remediation is applicable only in certain platforms if rpm --quiet -q openssh; then cp="CRYPTO_POLICY='-oCiphers=aes256-ctr,aes128-ctr,aes256-cbc,aes128-cbc -oMACs=hmac-sha2-512,hmac-sha2-256 -oGSSAPIKeyExchange=no -oKexAlgorithms=ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group14-sha1 -oHostKeyAlgorithms=ssh-rsa,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256 -oPubkeyAcceptedKeyTypes=rsa-sha2-512,rsa-sha2-256,ssh-rsa,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256'" file=/etc/crypto-policies/local.d/opensshserver-ospp.config #blank line at the begining to ease later readibility echo '' > "$file" echo "$cp" >> "$file" update-crypto-policies else >&2 echo 'Remediation is not applicable, nothing was done' fi Configure SSH Client to Use FIPS 140-2 Validated MACs: openssh.config Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSH is supported by system crypto policy, but the OpenSSH configuration may be set up incorrectly. To check that Crypto Policies settings are configured correctly, ensure that /etc/crypto-policies/back-ends/openssh.config contains the following line and is not commented out: MACs The system needs to be rebooted for these changes to take effect. System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process. AC-17(2) SRG-OS-000125-GPOS-00065 SRG-OS-000250-GPOS-00093 Overriding the system crypto policy makes the behavior of the OpenSSH client violate expectations, and makes system configuration more fragmented. sshd_approved_macs='' if [ -e "/etc/crypto-policies/back-ends/openssh.config" ] ; then LC_ALL=C sed -i "/^.*MACs\s\+/d" "/etc/crypto-policies/back-ends/openssh.config" else touch "/etc/crypto-policies/back-ends/openssh.config" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/crypto-policies/back-ends/openssh.config" cp "/etc/crypto-policies/back-ends/openssh.config" "/etc/crypto-policies/back-ends/openssh.config.bak" # Insert at the end of the file printf '%s\n' "MACs ${sshd_approved_macs}" >> "/etc/crypto-policies/back-ends/openssh.config" # Clean up after ourselves. rm "/etc/crypto-policies/back-ends/openssh.config.bak" - name: XCCDF Value sshd_approved_macs # promote to variable set_fact: sshd_approved_macs: !!str tags: - always - name: 'Configure SSH Daemon to Use FIPS 140-2 Validated MACs: openssh.config' block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/crypto-policies/back-ends/openssh.config create: true regexp: (?i)^.*MACs\s+ state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/crypto-policies/back-ends/openssh.config ansible.builtin.lineinfile: path: /etc/crypto-policies/back-ends/openssh.config create: true regexp: (?i)^.*MACs\s+ state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/crypto-policies/back-ends/openssh.config ansible.builtin.lineinfile: path: /etc/crypto-policies/back-ends/openssh.config create: true regexp: (?i)^.*MACs\s+ line: MACs {{ sshd_approved_macs }} state: present tags: - NIST-800-53-AC-17(2) - harden_sshd_macs_openssh_conf_crypto_policy - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Operating System Vendor Support and Certification The assurance of a vendor to provide operating system support and maintenance for their product is an important criterion to ensure product stability and security over the life of the product. A certified product that follows the necessary standards and government certification requirements guarantees that known software vulnerabilities will be remediated, and proper guidance for protecting and securing the operating system will be given. The Installed Operating System Is FIPS 140-2 Certified To enable processing of sensitive information the operating system must provide certified cryptographic modules compliant with FIPS 140-2 standard. There is no remediation besides switching to a different operating system. System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process. CIP-003-8 R4.2 CIP-007-3 R5.1 SC-12(2) SC-12(3) IA-7 SC-13 CM-6(a) SC-12 The Federal Information Processing Standard (FIPS) Publication 140-2, (FIPS PUB 140-2) is a computer security standard. The standard specifies security requirements for cryptographic modules used to protect sensitive unclassified information. Refer to the full FIPS 140-2 standard at http://csrc.nist.gov/publications/fips/fips140-2/fips1402.pdf for further details on the requirements. FIPS 140-2 validation is required by U.S. law when information systems use cryptography to protect sensitive government information. In order to achieve FIPS 140-2 certification, cryptographic modules are subject to extensive testing by independent laboratories, accredited by National Institute of Standards and Technology (NIST). The Installed Operating System Is Vendor Supported The installed operating system must be maintained by a vendor. Red Hat Enterprise Linux is supported by Red Hat, Inc. As the Red Hat Enterprise Linux vendor, Red Hat, Inc. is responsible for providing security patches. There is no remediation besides switching to a different operating system. 18 20 4 APO12.01 APO12.02 APO12.03 APO12.04 BAI03.10 DSS05.01 DSS05.02 4.2.3 4.2.3.12 4.2.3.7 4.2.3.9 A.12.6.1 A.14.2.3 A.16.1.3 A.18.2.2 A.18.2.3 CM-6(a) MA-6 SA-13(a) ID.RA-1 PR.IP-12 SRG-OS-000480-GPOS-00227 An operating system is considered "supported" if the vendor continues to provide security patches for the product. With an unsupported release, it will not be possible to resolve any security issue discovered in the system software. Endpoint Protection Software Endpoint protection security software that is not provided or supported by Red Hat can be installed to provide complementary or duplicative security capabilities to those provided by the base platform. Add-on software may not be appropriate for some specialized systems. Configure Backups of User Data The operating system must conduct backups of user data contained in the operating system. The operating system provides utilities for automating backups of user data. Commercial and open-source products are also available. Operating system backup is a critical step in maintaining data assurance and availability. User-level information is data generated by information system and/or application users. Backups shall be consistent with organizational recovery time and recovery point objectives. Install Virus Scanning Software Virus scanning software can be used to protect a system from penetration from computer viruses and to limit their spread through intermediate systems. The virus scanning software should be configured to perform scans dynamically on accessed files. If this capability is not available, the system must be configured to scan, at a minimum, all altered files on the system on a daily basis. If the system processes inbound SMTP mail, the virus scanner must be configured to scan all received mail. 12 13 14 4 7 8 APO01.06 APO13.02 BAI02.01 BAI06.01 DSS04.07 DSS05.01 DSS05.02 DSS05.03 DSS06.06 4.3.4.3.8 4.4.3.2 SR 3.2 SR 3.3 SR 3.4 SR 4.1 A.12.2.1 A.14.2.8 A.8.2.3 CM-6(a) DE.CM-4 DE.DP-3 PR.DS-1 SRG-OS-000480-GPOS-00227 Virus scanning software can be used to detect if a system has been compromised by computer viruses, as well as to limit their spread to other systems. Install Intrusion Detection Software The base Fedora platform already includes a sophisticated auditing system that can detect intruder activity, as well as SELinux, which provides host-based intrusion prevention capabilities by confining privileged programs and user sessions which may become compromised. In DoD environments, supplemental intrusion detection and antivirus tools, such as the McAfee Host-based Security System, are available to integrate with existing infrastructure. Per DISA guidance, when these supplemental tools interfere with proper functioning of SELinux, SELinux takes precedence. Should further clarification be required, DISA contact information is published publicly at https://www.cyber.mil/stigs/ 1 12 13 14 15 16 18 7 8 9 APO01.06 APO13.01 DSS01.03 DSS01.05 DSS03.05 DSS05.02 DSS05.04 DSS05.07 DSS06.02 4.3.3.4 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) DE.CM-1 PR.AC-5 PR.DS-5 PR.PT-4 Req-11.4 Host-based intrusion detection tools provide a system-level defense when an intruder gains access to a system or network. McAfee Endpoint Security Software In DoD environments, McAfee Host-based Security System (HBSS) and VirusScan Enterprise for Linux (VSEL) is required to be installed on all systems. McAfee Host-Based Intrusion Detection Software (HBSS) McAfee Host-based Security System (HBSS) is a suite of software applications used to monitor, detect, and defend computer networks and systems. Install the Host Intrusion Prevention System (HIPS) Module Install the McAfee Host Intrusion Prevention System (HIPS) Module if it is absolutely necessary. If SELinux is enabled, do not install or enable this module. Installing and enabling this module conflicts with SELinux. Per profile guidance, SELinux takes precedence over this module. Due to McAfee HIPS being 3rd party software, automated remediation is not available for this configuration check. 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 APO01.06 APO07.06 APO08.04 APO10.05 APO11.06 APO12.01 APO12.02 APO12.03 APO12.04 APO12.06 APO13.01 APO13.02 BAI08.02 BAI08.04 DSS01.03 DSS01.05 DSS02.04 DSS02.05 DSS02.07 DSS03.01 DSS03.04 DSS03.05 DSS04.05 DSS05.01 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.01 DSS06.02 MEA03.03 MEA03.04 4.2.3 4.2.3.12 4.2.3.7 4.2.3.9 4.3.3.4 4.3.4.5.2 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.3.4.5.9 4.4.3.2 4.4.3.3 4.4.3.4 SR 2.10 SR 2.11 SR 2.12 SR 2.4 SR 2.8 SR 2.9 SR 3.1 SR 3.3 SR 3.5 SR 3.8 SR 3.9 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.4.1 A.12.4.3 A.12.5.1 A.12.6.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.7 A.14.2.8 A.15.2.1 A.16.1.1 A.16.1.2 A.16.1.3 A.16.1.4 A.16.1.5 A.16.1.6 A.16.1.7 A.18.1.4 A.18.2.2 A.18.2.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 Clause 16.1.2 Clause 7.4 CM-6(a) DE.AE-1 DE.AE-2 DE.AE-3 DE.AE-4 DE.CM-1 DE.CM-5 DE.CM-6 DE.CM-7 DE.DP-2 DE.DP-3 DE.DP-4 DE.DP-5 ID.RA-1 PR.AC-5 PR.DS-5 PR.IP-8 PR.PT-4 RS.AN-1 RS.CO-3 Req-11.4 SRG-OS-000191-GPOS-00080 SRG-OS-000196 SRG-OS-000480-GPOS-00227 Without a host-based intrusion detection tool, there is no system-level defense when an intruder gains access to a system or network. Additionally, a host-based intrusion prevention tool can provide methods to immediately lock out detected intrusion attempts. package install MFEhiplsm dnf install MFEhiplsm Disk Partitioning To ensure separation and protection of data, there are top-level system directories which should be placed on their own physical partition or logical volume. The installer's default partitioning scheme creates separate logical volumes for /, /boot, and swap. If starting with any of the default layouts, check the box to \"Review and modify partitioning.\" This allows for the easy creation of additional logical volumes inside the volume group already created, though it may require making /'s logical volume smaller to create space. In general, using logical volumes is preferable to using partitions because they can be more easily adjusted later.If creating a custom layout, create the partitions mentioned in the previous paragraph (which the installer will require anyway), as well as separate ones described in the following sections. If a system has already been installed, and the default partitioning scheme was used, it is possible but nontrivial to modify it to create separate logical volumes for the directories listed above. The Logical Volume Manager (LVM) makes this possible. Encrypt Partitions Fedora natively supports partition encryption through the Linux Unified Key Setup-on-disk-format (LUKS) technology. The easiest way to encrypt a partition is during installation time. For manual installations, select the Encrypt checkbox during partition creation to encrypt the partition. When this option is selected the system will prompt for a passphrase to use in decrypting the partition. The passphrase will subsequently need to be entered manually every time the system boots. For automated/unattended installations, it is possible to use Kickstart by adding the --encrypted and --passphrase= options to the definition of each partition to be encrypted. For example, the following line would encrypt the root partition: part / --fstype=ext4 --size=100 --onpart=hda1 --encrypted --passphrase=PASSPHRASE Any PASSPHRASE is stored in the Kickstart in plaintext, and the Kickstart must then be protected accordingly. Omitting the --passphrase= option from the partition definition will cause the installer to pause and interactively ask for the passphrase during installation. By default, the Anaconda installer uses aes-xts-plain64 cipher with a minimum 512 bit key size which should be compatible with FIPS enabled. Detailed information on encrypting partitions using LUKS or LUKS ciphers can be found on the Fedora Documentation web site: https://docs.fedoraproject.org/en-US/quick-docs/encrypting-drives-using-LUKS/ . 13 14 APO01.06 BAI02.01 BAI06.01 DSS04.07 DSS05.03 DSS05.04 DSS05.07 DSS06.02 DSS06.06 3.13.16 164.308(a)(1)(ii)(D) 164.308(b)(1) 164.310(d) 164.312(a)(1) 164.312(a)(2)(iii) 164.312(a)(2)(iv) 164.312(b) 164.312(c) 164.314(b)(2)(i) 164.312(d) SR 3.4 SR 4.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R4.2 CIP-007-3 R5.1 CM-6(a) SC-28 SC-28(1) SC-13 AU-9(3) PR.DS-1 PR.DS-5 SRG-OS-000405-GPOS-00184 SRG-OS-000185-GPOS-00079 SRG-OS-000404-GPOS-00183 The risk of a system's physical compromise, particularly mobile systems such as laptops, places its data at risk of compromise. Encrypting this data mitigates the risk of its loss if the system is lost. Ensure /boot Located On Separate Partition It is recommended that the /boot directory resides on a separate partition. This makes it easier to apply restrictions e.g. through the noexec mount option. Eventually, the /boot partition can be configured not to be mounted automatically with the noauto mount option. R28 The /boot partition contains the kernel and bootloader files. Access to this partition should be restricted. part /boot [[customizations.filesystem]] mountpoint = "/boot" size = 1073741824 Ensure /dev/shm is configured The /dev/shm is a traditional shared memory concept. One program will create a memory portion, which other processes (if permitted) can access. If /dev/shm is not configured, tmpfs will be mounted to /dev/shm by systemd. This rule does not have a remedation. It is expected that this will be managed by systemd and will be a tmpfs partition. 1.1.2.2.1 Any user can upload and execute files inside the /dev/shm similar to the /tmp partition. Configuring /dev/shm allows an administrator to set the noexec option on the mount, making /dev/shm useless for an attacker to install executable code. It would also prevent an attacker from establishing a hardlink to a system setuid program and wait for it to be updated. Once the program was updated, the hardlink would be broken and the attacker would have his own copy of the program. If the program happened to have a security vulnerability, the attacker could continue to exploit the known flaw. Ensure /home Located On Separate Partition If user home directories will be stored locally, create a separate partition for /home at installation time (or migrate it later using LVM). If /home will be mounted from another system such as an NFS server, then creating a separate partition is not necessary at installation time, and the mountpoint can instead be configured later. 12 15 8 APO13.01 DSS05.02 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.13.1.1 A.13.2.1 A.14.1.3 CM-6(a) SC-5(2) PR.PT-4 SRG-OS-000480-GPOS-00227 R28 1.1.2.3.1 Ensuring that /home is mounted on its own partition enables the setting of more restrictive mount options, and also helps ensure that users cannot trivially fill partitions used for log or audit data storage. part /home [[customizations.filesystem]] mountpoint = "/home" size = 1073741824 logvol /home 1024 Ensure /opt Located On Separate Partition It is recommended that the /opt directory resides on a separate partition. R28 The /opt partition contains additional software, usually installed outside the packaging system. Putting this directory on a separate partition makes it easier to apply restrictions e.g. through the nosuid mount option. part /opt [[customizations.filesystem]] mountpoint = "/opt" size = 1073741824 logvol /opt 1024 Ensure /srv Located On Separate Partition If a file server (FTP, TFTP...) is hosted locally, create a separate partition for /srv at installation time (or migrate it later using LVM). If /srv will be mounted from another system such as an NFS server, then creating a separate partition is not necessary at installation time, and the mountpoint can instead be configured later. R28 Srv deserves files for local network file server such as FTP. Ensuring that /srv is mounted on its own partition enables the setting of more restrictive mount options, and also helps ensure that users cannot trivially fill partitions used for log or audit data storage. part /srv [[customizations.filesystem]] mountpoint = "/srv" size = 1073741824 logvol /srv 1024 Ensure /tmp Located On Separate Partition The /tmp directory is a world-writable directory used for temporary file storage. Ensure it has its own partition or logical volume at installation time, or migrate it using LVM. 12 15 8 APO13.01 DSS05.02 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.13.1.1 A.13.2.1 A.14.1.3 CM-6(a) SC-5(2) PR.PT-4 SRG-OS-000480-GPOS-00227 1.1.2.1.1 The /tmp partition is used as temporary storage by many programs. Placing /tmp in its own partition enables the setting of more restrictive mount options, which can help protect programs which use it. part /tmp [[customizations.filesystem]] mountpoint = "/tmp" size = 1073741824 logvol /tmp 1024 Ensure /usr Located On Separate Partition It is recommended that the /usr directory resides on a separate partition. R28 The /usr partition contains system software, utilities and files. Putting it on a separate partition allows limiting its size and applying restrictions through mount options. part /usr [[customizations.filesystem]] mountpoint = "/usr" size = 5368709120 logvol /usr 5120 Ensure /var Located On Separate Partition The /var directory is used by daemons and other system services to store frequently-changing data. Ensure that /var has its own partition or logical volume at installation time, or migrate it using LVM. 12 15 8 APO13.01 DSS05.02 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.13.1.1 A.13.2.1 A.14.1.3 CM-6(a) SC-5(2) PR.PT-4 SRG-OS-000480-GPOS-00227 R28 1.1.2.4.1 Ensuring that /var is mounted on its own partition enables the setting of more restrictive mount options. This helps protect system services such as daemons or other programs which use it. It is not uncommon for the /var directory to contain world-writable directories installed by other software packages. part /var [[customizations.filesystem]] mountpoint = "/var" size = 3221225472 logvol /var 3072 Ensure /var/log Located On Separate Partition System logs are stored in the /var/log directory. Ensure that /var/log has its own partition or logical volume at installation time, or migrate it using LVM. 1 12 14 15 16 3 5 6 8 APO11.04 APO13.01 BAI03.05 DSS05.02 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 CIP-007-3 R6.5 CM-6(a) AU-4 SC-5(2) PR.PT-1 PR.PT-4 SRG-OS-000480-GPOS-00227 R28 1.1.2.6.1 Placing /var/log in its own partition enables better separation between log files and other files in /var/. part /var/log [[customizations.filesystem]] mountpoint = "/var/log" size = 1073741824 logvol /var/log 1024 Ensure /var/log/audit Located On Separate Partition Audit logs are stored in the /var/log/audit directory. Ensure that /var/log/audit has its own partition or logical volume at installation time, or migrate it using LVM. Make absolutely certain that it is large enough to store all audit logs that will be created by the auditing daemon. 1 12 13 14 15 16 2 3 5 6 8 APO11.04 APO13.01 BAI03.05 BAI04.04 DSS05.02 DSS05.04 DSS05.07 MEA02.01 164.312(a)(2)(ii) 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.2 SR 7.6 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.17.2.1 CIP-007-3 R6.5 CM-6(a) AU-4 SC-5(2) PR.DS-4 PR.PT-1 PR.PT-4 FMT_SMF_EXT.1 SRG-OS-000341-GPOS-00132 SRG-OS-000480-GPOS-00227 SRG-APP-000357-CTR-000800 R71 1.1.2.7.1 Placing /var/log/audit in its own partition enables better separation between audit files and other files, and helps ensure that auditing cannot be halted due to the partition running out of space. part /var/log/audit [[customizations.filesystem]] mountpoint = "/var/log/audit" size = 10737418240 logvol /var/log/audit 10240 Ensure /var/tmp Located On Separate Partition The /var/tmp directory is a world-writable directory used for temporary file storage. Ensure it has its own partition or logical volume at installation time, or migrate it using LVM. SRG-OS-000480-GPOS-00227 R28 1.1.2.5.1 The /var/tmp partition is used as temporary storage by many programs. Placing /var/tmp in its own partition enables the setting of more restrictive mount options, which can help protect programs which use it. part /var/tmp [[customizations.filesystem]] mountpoint = "/var/tmp" size = 1073741824 logvol /var/tmp 1024 GNOME Desktop Environment GNOME is a graphical desktop environment bundled with many Linux distributions that allow users to easily interact with the operating system graphically rather than textually. The GNOME Graphical Display Manager (GDM) provides login, logout, and user switching contexts as well as display server management. GNOME is developed by the GNOME Project and is considered the default Red Hat Graphical environment. For more information on GNOME and the GNOME Project, see https://www.gnome.org. Remove the GDM Package Group By removing the gdm package, the system no longer has GNOME installed installed. If X Windows is not installed then the system cannot boot into graphical user mode. This prevents the system from being accidentally or maliciously booted into a graphical.target mode. To do so, run the following command: $ sudo yum remove gdm CM-7(a) CM-7(b) CM-6(a) SRG-OS-000480-GPOS-00227 2.1.22 Unnecessary service packages must not be installed to decrease the attack surface of the system. A graphical environment is unnecessary for certain types of systems including a virtualization hypervisor. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # CAUTION: This remediation script will remove gdm # from the system, and may remove any packages # that depend on gdm. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "gdm" ; then dnf remove -y --noautoremove "gdm" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_gdm_removed - name: 'Remove the GDM Package Group: Ensure gdm is removed' ansible.builtin.package: name: gdm state: absent when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_gdm_removed include remove_gdm class remove_gdm { package { 'gdm': ensure => 'purged', } } package --remove=gdm package remove gdm dnf remove gdm Make sure that the dconf databases are up-to-date with regards to respective keyfiles By default, DConf uses a binary database as a data backend. The system-level database is compiled from keyfiles in the /etc/dconf/db/ directory by the dconf update command. More specifically, content present in the following directories: /etc/dconf/db/distro.d /etc/dconf/db/local.d 164.308(a)(1)(ii)(B) 164.308(a)(5)(ii)(A) Req-6.2 SRG-OS-000480-GPOS-00227 reload_dconf_db 8.2.8 8.2 Unlike text-based keyfiles, the binary database is impossible to check by OVAL. Therefore, in order to evaluate dconf configuration, both have to be true at the same time - configuration files have to be compliant, and the database needs to be more recent than those keyfiles, which gives confidence that it reflects them. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm && { rpm --quiet -q kernel; }; then dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-6.2 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_db_up_to_date - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Run dconf update ansible.builtin.command: cmd: dconf update when: - '"gdm" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-6.2 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_db_up_to_date - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy Configure GNOME3 DConf User Profile By default, DConf provides a standard user profile. This profile contains a list of DConf configuration databases. The user profile and database always take the highest priority. As such the DConf User profile should always exist and be configured correctly. To make sure that the user profile is configured correctly, the /etc/dconf/profile/user should be set as follows: user-db:user system-db:local system-db:site system-db:distro 8.2.8 8.2 Failure to have a functional DConf profile prevents GNOME3 configuration settings from being enforced for all users and allows various security risks. Configure GNOME Login Screen In the default GNOME desktop, the login is displayed after system boot and can display user accounts, allow users to reboot the system, and allow users to login automatically and/or with a guest account. The login screen should be configured to prevent such behavior. For more information about enforcing preferences in the GNOME3 environment using the DConf configuration system, see https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/desktop_migration_and_administration_guide/> and the man page dconf(1). Disable the GNOME3 Login Restart and Shutdown Buttons In the default graphical environment, users logging directly into the system are greeted with a login screen that allows any user, known or unknown, the ability the ability to shutdown or restart the system. This functionality should be disabled by setting disable-restart-buttons to true. To disable, add or edit disable-restart-buttons to /etc/dconf/db/distro.d/00-security-settings. For example: [org/gnome/login-screen] disable-restart-buttons=true Once the setting has been added, add a lock to /etc/dconf/db/distro.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/login-screen/disable-restart-buttons After the settings have been set, run dconf update. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.2 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) CM-7(b) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 A user who is at the console can reboot the system at the login screen. If restart or shutdown buttons are pressed at the login screen, this can create the risk of short-term loss of availability of systems due to reboot. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/distro.d/00-security-settings" DBDIR="/etc/dconf/db/distro.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*disable-restart-buttons\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)disable-restart-buttons(\s*=)/#\1disable-restart-buttons\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*disable-restart-buttons\\s*=" "${DCONFFILE}" then sed -i "s/\\s*disable-restart-buttons\\s*=\\s*.*/disable-restart-buttons=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/login-screen\\]|a\\disable-restart-buttons=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/login-screen/disable-restart-buttons$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/distro.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/login-screen/disable-restart-buttons$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/login-screen/disable-restart-buttons$" /etc/dconf/db/distro.d/ then echo "/org/gnome/login-screen/disable-restart-buttons" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_restart_shutdown - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Disable the GNOME3 Login Restart and Shutdown Buttons community.general.ini_file: dest: /etc/dconf/db/distro.d/00-security-settings section: org/gnome/login-screen option: disable-restart-buttons value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_restart_shutdown - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME disablement of Login Restart and Shutdown Buttons ansible.builtin.lineinfile: path: /etc/dconf/db/distro.d/locks/00-security-settings-lock regexp: ^/org/gnome/login-screen/disable-restart-buttons line: /org/gnome/login-screen/disable-restart-buttons create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_restart_shutdown - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_restart_shutdown - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy Disable the GNOME3 Login User List In the default graphical environment, users logging directly into the system are greeted with a login screen that displays all known users. This functionality should be disabled by setting disable-user-list to true. To disable, add or edit disable-user-list to /etc/dconf/db/distro.d/00-security-settings. For example: [org/gnome/login-screen] disable-user-list=true Once the setting has been added, add a lock to /etc/dconf/db/distro.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/login-screen/disable-user-list After the settings have been set, run dconf update. CM-6(a) AC-23 SRG-OS-000480-GPOS-00227 1.8.2 Leaving the user list enabled is a security risk since it allows anyone with physical access to the system to quickly enumerate known user accounts without logging in. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/distro.d/00-security-settings" DBDIR="/etc/dconf/db/distro.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*disable-user-list\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)disable-user-list(\s*=)/#\1disable-user-list\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*disable-user-list\\s*=" "${DCONFFILE}" then sed -i "s/\\s*disable-user-list\\s*=\\s*.*/disable-user-list=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/login-screen\\]|a\\disable-user-list=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/login-screen/disable-user-list$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/distro.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/login-screen/disable-user-list$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/login-screen/disable-user-list$" /etc/dconf/db/distro.d/ then echo "/org/gnome/login-screen/disable-user-list" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-23 - NIST-800-53-CM-6(a) - dconf_gnome_disable_user_list - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable the GNOME3 Login User List community.general.ini_file: dest: /etc/dconf/db/distro.d/00-security-settings section: org/gnome/login-screen option: disable-user-list value: 'true' no_extra_spaces: true create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-AC-23 - NIST-800-53-CM-6(a) - dconf_gnome_disable_user_list - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 disablement of Login User List ansible.builtin.lineinfile: path: /etc/dconf/db/distro.d/locks/00-security-settings-lock regexp: ^/org/gnome/login-screen/disable-user-list$ line: /org/gnome/login-screen/disable-user-list create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-AC-23 - NIST-800-53-CM-6(a) - dconf_gnome_disable_user_list - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-AC-23 - NIST-800-53-CM-6(a) - dconf_gnome_disable_user_list - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Enable the GNOME3 Login Smartcard Authentication In the default graphical environment, smart card authentication can be enabled on the login screen by setting enable-smartcard-authentication to true. To enable, add or edit enable-smartcard-authentication to /etc/dconf/db/distro.d/00-security-settings. For example: [org/gnome/login-screen] enable-smartcard-authentication=true Once the setting has been added, add a lock to /etc/dconf/db/distro.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/login-screen/enable-smartcard-authentication After the settings have been set, run dconf update. IA-2(3) IA-2(4) IA-2(8) IA-2(9) IA-2(11) Req-8.3 SRG-OS-000375-GPOS-00160 SRG-OS-000376-GPOS-00161 SRG-OS-000377-GPOS-00162 Smart card login provides two-factor authentication stronger than that provided by a username and password combination. Smart cards leverage PKI (public key infrastructure) in order to provide and verify credentials. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/distro.d/00-security-settings" DBDIR="/etc/dconf/db/distro.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*enable-smartcard-authentication\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)enable-smartcard-authentication(\s*=)/#\1enable-smartcard-authentication\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*enable-smartcard-authentication\\s*=" "${DCONFFILE}" then sed -i "s/\\s*enable-smartcard-authentication\\s*=\\s*.*/enable-smartcard-authentication=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/login-screen\\]|a\\enable-smartcard-authentication=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/login-screen/enable-smartcard-authentication$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/distro.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/login-screen/enable-smartcard-authentication$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/login-screen/enable-smartcard-authentication$" /etc/dconf/db/distro.d/ then echo "/org/gnome/login-screen/enable-smartcard-authentication" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(8) - NIST-800-53-IA-2(9) - PCI-DSS-Req-8.3 - dconf_gnome_enable_smartcard_auth - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Enable the GNOME3 Login Smartcard Authentication community.general.ini_file: dest: /etc/dconf/db/distro.d/00-security-settings section: org/gnome/login-screen option: enable-smartcard-authentication value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(8) - NIST-800-53-IA-2(9) - PCI-DSS-Req-8.3 - dconf_gnome_enable_smartcard_auth - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 disablement of Smartcard Authentication ansible.builtin.lineinfile: path: /etc/dconf/db/distro.d/locks/00-security-settings-lock regexp: ^/org/gnome/login-screen/enable-smartcard-authentication$ line: /org/gnome/login-screen/enable-smartcard-authentication create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(8) - NIST-800-53-IA-2(9) - PCI-DSS-Req-8.3 - dconf_gnome_enable_smartcard_auth - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(8) - NIST-800-53-IA-2(9) - PCI-DSS-Req-8.3 - dconf_gnome_enable_smartcard_auth - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Enable the GNOME3 Screen Locking On Smartcard Removal In the default graphical environment, screen locking on smartcard removal can be enabled by setting removal-action to 'lock-screen'. To enable, add or edit removal-action to /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/settings-daemon/peripherals/smartcard] removal-action='lock-screen' Once the setting has been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/settings-daemon/peripherals/smartcard/removal-action After the settings have been set, run dconf update. SRG-OS-000028-GPOS-00009 SRG-OS-000030-GPOS-00011 Locking the screen automatically when removing the smartcard can prevent undesired access to system. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/settings-daemon/peripherals/smartcard\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*removal-action\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)removal-action(\s*=)/#\1removal-action\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/settings-daemon/peripherals/smartcard\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/settings-daemon/peripherals/smartcard]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "'lock-screen'")" if grep -q "^\\s*removal-action\\s*=" "${DCONFFILE}" then sed -i "s/\\s*removal-action\\s*=\\s*.*/removal-action=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/settings-daemon/peripherals/smartcard\\]|a\\removal-action=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$" /etc/dconf/db/local.d/ then echo "/org/gnome/settings-daemon/peripherals/smartcard/removal-action" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Detect if removal-action can be found on /etc/dconf/db/local.d/ ansible.builtin.find: path: /etc/dconf/db/local.d/ contains: ^\s*removal-action register: dconf_gnome_lock_screen_on_smartcard_removal_config_files when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Configure removal-action - default file community.general.ini_file: dest: /etc/dconf/db/local.d//00-security-settings section: org/gnome/settings-daemon/peripherals/smartcard option: removal-action value: '''lock-screen''' create: true when: - '"gdm" in ansible_facts.packages' - dconf_gnome_lock_screen_on_smartcard_removal_config_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_config_files.matched == 0 tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Configure removal-action - existing files community.general.ini_file: dest: '{{ item.path }}' section: org/gnome/settings-daemon/peripherals/smartcard option: removal-action value: '''lock-screen''' create: true with_items: '{{ dconf_gnome_lock_screen_on_smartcard_removal_config_files.files }}' when: - '"gdm" in ansible_facts.packages' - dconf_gnome_lock_screen_on_smartcard_removal_config_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_config_files.matched > 0 tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Detect if lock for removal-action can be found on /etc/dconf/db/local.d/ ansible.builtin.find: path: /etc/dconf/db/local.d/locks contains: ^\s*removal-action register: dconf_gnome_lock_screen_on_smartcard_removal_lock_files when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification removal-action - default file ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$ line: /org/gnome/settings-daemon/peripherals/smartcard/removal-action create: true when: - '"gdm" in ansible_facts.packages' - dconf_gnome_lock_screen_on_smartcard_removal_lock_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_lock_files.matched == 0 tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification removal-action - existing files ansible.builtin.lineinfile: path: '{{ item.path }}' regexp: ^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$ line: /org/gnome/settings-daemon/peripherals/smartcard/removal-action create: true with_items: '{{ dconf_gnome_lock_screen_on_smartcard_removal_lock_files.files }}' when: - '"gdm" in ansible_facts.packages' - dconf_gnome_lock_screen_on_smartcard_removal_lock_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_lock_files.matched > 0 tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update - removal-action ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_lock_screen_on_smartcard_removal - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Set the GNOME3 Login Number of Failures In the default graphical environment, the GNOME3 login screen and be configured to restart the authentication process after a configured number of attempts. This can be configured by setting allowed-failures to 3 or less. To enable, add or edit allowed-failures to /etc/dconf/db/distro.d/00-security-settings. For example: [org/gnome/login-screen] allowed-failures=3 Once the setting has been added, add a lock to /etc/dconf/db/distro.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/login-screen/allowed-failures After the settings have been set, run dconf update. 3.1.8 Setting the password retry prompts that are permitted on a per-session basis to a low value requires some software, such as SSH, to re-connect. This can slow down and draw additional attention to some types of password-guessing attacks. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/distro.d/00-security-settings" DBDIR="/etc/dconf/db/distro.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*allowed-failures\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)allowed-failures(\s*=)/#\1allowed-failures\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "3")" if grep -q "^\\s*allowed-failures\\s*=" "${DCONFFILE}" then sed -i "s/\\s*allowed-failures\\s*=\\s*.*/allowed-failures=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/login-screen\\]|a\\allowed-failures=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/login-screen/allowed-failures$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/distro.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/login-screen/allowed-failures$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/login-screen/allowed-failures$" /etc/dconf/db/distro.d/ then echo "/org/gnome/login-screen/allowed-failures" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.8 - dconf_gnome_login_retries - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Enable the GNOME3 Login Number of Failures community.general.ini_file: dest: /etc/dconf/db/distro.d/00-security-settings section: org/gnome/login-screen option: allowed-failures value: '3' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.8 - dconf_gnome_login_retries - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 Login Number of Failures ansible.builtin.lineinfile: path: /etc/dconf/db/distro.d/locks/00-security-settings-lock regexp: ^/org/gnome/login-screen/allowed-failures$ line: /org/gnome/login-screen/allowed-failures create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.8 - dconf_gnome_login_retries - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.8 - dconf_gnome_login_retries - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Disable GDM Automatic Login The GNOME Display Manager (GDM) can allow users to automatically login without user interaction or credentials. User should always be required to authenticate themselves to the system that they are authorized to use. To disable user ability to automatically login to the system, set the AutomaticLoginEnable to false in the [daemon] section in /etc/gdm/custom.conf. For example: [daemon] AutomaticLoginEnable=false 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.1 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-6(a) AC-6(1) CM-7(b) PR.IP-1 SRG-OS-000480-GPOS-00229 8.3.1 8.3 Failure to restrict system access to authenticated users negatively impacts operating system security. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then if rpm --quiet -q gdm then if ! grep -q "^AutomaticLoginEnable=" /etc/gdm/custom.conf then sed -i "/^\[daemon\]/a \ AutomaticLoginEnable=False" /etc/gdm/custom.conf else sed -i "s/^AutomaticLoginEnable=.*/AutomaticLoginEnable=False/g" /etc/gdm/custom.conf fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.1 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - gnome_gdm_disable_automatic_login - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Disable GDM Automatic Login community.general.ini_file: dest: /etc/gdm/custom.conf section: daemon option: AutomaticLoginEnable value: 'false' no_extra_spaces: true create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.1 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - gnome_gdm_disable_automatic_login - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy Disable GDM Guest Login The GNOME Display Manager (GDM) can allow users to login without credentials which can be useful for public kiosk scenarios. Allowing users to login without credentials or "guest" account access has inherent security risks and should be disabled. To do disable timed logins or guest account access, set the TimedLoginEnable to false in the [daemon] section in /etc/gdm/custom.conf. For example: [daemon] TimedLoginEnable=false 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.1 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-7(a) CM-7(b) CM-6(a) IA-2 PR.IP-1 SRG-OS-000480-GPOS-00229 8.3.1 8.3 Failure to restrict system access to authenticated users negatively impacts operating system security. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then if rpm --quiet -q gdm then if ! grep -q "^TimedLoginEnable=" /etc/gdm/custom.conf then sed -i "/^\[daemon\]/a \ TimedLoginEnable=false" /etc/gdm/custom.conf else sed -i "s/^TimedLoginEnable=.*/TimedLoginEnable=false/g" /etc/gdm/custom.conf fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.1 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-IA-2 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - gnome_gdm_disable_guest_login - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Disable GDM Guest Login community.general.ini_file: dest: /etc/gdm/custom.conf section: daemon option: TimedLoginEnable value: 'false' no_extra_spaces: true create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.1 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-IA-2 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - gnome_gdm_disable_guest_login - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy Disable XDMCP in GDM XDMCP is an unencrypted protocol, and therefore, presents a security risk, see e.g. XDMCP Gnome docs. To disable XDMCP support in Gnome, set Enable to false under the [xdmcp] configuration section in /etc/gdm/custom.conf. For example: [xdmcp] Enable=false XDMCP provides unencrypted remote access through the Gnome Display Manager (GDM) which does not provide for the confidentiality and integrity of user passwords or the remote session. If a privileged user were to login using XDMCP, the privileged user password could be compromised due to typed XEvents and keystrokes will traversing over the network in clear text. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Try find '[xdmcp]' and 'Enable' in '/etc/gdm/custom.conf', if it exists, set # to 'false', if it isn't here, add it, if '[xdmcp]' doesn't exist, add it there if grep -qzosP '[[:space:]]*\[xdmcp]([^\n\[]*\n+)+?[[:space:]]*Enable' '/etc/gdm/custom.conf'; then sed -i "s/Enable[^(\n)]*/Enable=false/" '/etc/gdm/custom.conf' elif grep -qs '[[:space:]]*\[xdmcp]' '/etc/gdm/custom.conf'; then sed -i "/[[:space:]]*\[xdmcp]/a Enable=false" '/etc/gdm/custom.conf' else if test -d "/etc/gdm"; then printf '%s\n' '[xdmcp]' "Enable=false" >> '/etc/gdm/custom.conf' else echo "Config file directory '/etc/gdm' doesnt exist, not remediating, assuming non-applicability." >&2 fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - gnome_gdm_disable_xdmcp - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Disable XDMCP in GDM community.general.ini_file: path: /etc/gdm/custom.conf section: xdmcp option: Enable value: 'false' create: true mode: 420 when: '"gdm" in ansible_facts.packages' tags: - gnome_gdm_disable_xdmcp - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy GNOME Media Settings GNOME media settings that apply to the graphical interface. Disable GNOME3 Automounting The system's default desktop environment, GNOME3, will mount devices and removable media (such as DVDs, CDs and USB flash drives) whenever they are inserted into the system. To disable automount within GNOME3, add or set automount to false in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/media-handling] automount=false Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/media-handling/automount After the settings have been set, run dconf update. 12 16 APO13.01 DSS01.04 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 3.1.7 4.3.3.2.2 4.3.3.5.2 4.3.3.6.6 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.13 SR 1.2 SR 1.4 SR 1.5 SR 1.9 SR 2.1 SR 2.6 A.11.2.6 A.13.1.1 A.13.2.1 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.AC-6 SRG-OS-000114-GPOS-00059 SRG-OS-000378-GPOS-00163 SRG-OS-000480-GPOS-00227 1.8.4 3.4.2 3.4 Disabling automatic mounting in GNOME3 can prevent the introduction of malware via removable media. It will, however, also prevent desktop users from legitimate use of removable media. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/media-handling\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*automount\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)automount(\s*=)/#\1automount\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/media-handling\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/media-handling]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "false")" if grep -q "^\\s*automount\\s*=" "${DCONFFILE}" then sed -i "s/\\s*automount\\s*=\\s*.*/automount=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/media-handling\\]|a\\automount=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/media-handling/automount$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/media-handling/automount$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/media-handling/automount$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/media-handling/automount" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable GNOME3 Automounting - automount community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/media-handling option: automount value: 'false' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 Automounting - automount ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/media-handling/automount$ line: /org/gnome/desktop/media-handling/automount create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Disable GNOME3 Automount Opening The system's default desktop environment, GNOME3, will mount devices and removable media (such as DVDs, CDs and USB flash drives) whenever they are inserted into the system. To disable automount-open within GNOME3, add or set automount-open to false in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/media-handling] automount-open=false Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/media-handling/automount-open After the settings have been set, run dconf update. 12 16 APO13.01 DSS01.04 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 3.1.7 4.3.3.2.2 4.3.3.5.2 4.3.3.6.6 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.13 SR 1.2 SR 1.4 SR 1.5 SR 1.9 SR 2.1 SR 2.6 A.11.2.6 A.13.1.1 A.13.2.1 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.AC-6 SRG-OS-000114-GPOS-00059 SRG-OS-000378-GPOS-00163 SRG-OS-000480-GPOS-00227 1.8.4 3.4.2 3.4 Automatically mounting file systems permits easy introduction of unknown devices, thereby facilitating malicious activity. Disabling automatic mounting in GNOME3 can prevent the introduction of malware via removable media. It will, however, also prevent desktop users from legitimate use of removable media. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/media-handling\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*automount-open\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)automount-open(\s*=)/#\1automount-open\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/media-handling\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/media-handling]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "false")" if grep -q "^\\s*automount-open\\s*=" "${DCONFFILE}" then sed -i "s/\\s*automount-open\\s*=\\s*.*/automount-open=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/media-handling\\]|a\\automount-open=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/media-handling/automount-open$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/media-handling/automount-open$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/media-handling/automount-open$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/media-handling/automount-open" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount_open - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable GNOME3 Automounting - automount-open community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/media-handling option: automount-open value: 'false' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount_open - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 Automounting - automount-open ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/media-handling/automount-open$ line: /org/gnome/desktop/media-handling/automount-open create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount_open - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - dconf_gnome_disable_automount_open - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Disable GNOME3 Automount running The system's default desktop environment, GNOME3, will mount devices and removable media (such as DVDs, CDs and USB flash drives) whenever they are inserted into the system. To disable autorun-never within GNOME3, add or set autorun-never to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/media-handling] autorun-never=true Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/media-handling/autorun-never After the settings have been set, run dconf update. 12 16 APO13.01 DSS01.04 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 3.1.7 4.3.3.2.2 4.3.3.5.2 4.3.3.6.6 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.13 SR 1.2 SR 1.4 SR 1.5 SR 1.9 SR 2.1 SR 2.6 A.11.2.6 A.13.1.1 A.13.2.1 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.AC-6 SRG-OS-000114-GPOS-00059 SRG-OS-000378-GPOS-00163 SRG-OS-000480-GPOS-00227 1.8.5 Automatically mounting file systems permits easy introduction of unknown devices, thereby facilitating malicious activity. Disabling automatic mount running in GNOME3 can prevent the introduction of malware via removable media. It will, however, also prevent desktop users from legitimate use of removable media. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/media-handling\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*autorun-never\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)autorun-never(\s*=)/#\1autorun-never\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/media-handling\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/media-handling]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*autorun-never\\s*=" "${DCONFFILE}" then sed -i "s/\\s*autorun-never\\s*=\\s*.*/autorun-never=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/media-handling\\]|a\\autorun-never=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/media-handling/autorun-never$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/media-handling/autorun-never$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/media-handling/autorun-never$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/media-handling/autorun-never" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_autorun - low_complexity - low_severity - medium_disruption - no_reboot_needed - unknown_strategy - name: Disable GNOME3 Automounting - autorun-never community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/media-handling option: autorun-never value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_autorun - low_complexity - low_severity - medium_disruption - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 Automounting - autorun-never ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/media-handling/autorun-never$ line: /org/gnome/desktop/media-handling/autorun-never create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_autorun - low_complexity - low_severity - medium_disruption - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_autorun - low_complexity - low_severity - medium_disruption - no_reboot_needed - unknown_strategy Disable All GNOME3 Thumbnailers The system's default desktop environment, GNOME3, uses a number of different thumbnailer programs to generate thumbnails for any new or modified content in an opened folder. To disable the execution of these thumbnail applications, add or set disable-all to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/thumbnailers] disable-all=true Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/thumbnailers/disable-all After the settings have been set, run dconf update. This effectively prevents an attacker from gaining access to a system through a flaw in GNOME3's Nautilus thumbnail creators. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 An attacker with knowledge of a flaw in a GNOME3 thumbnailer application could craft a malicious file to exploit this flaw. Assuming the attacker could place the malicious file on the local filesystem (via a web upload for example) and assuming a user browses the same location using Nautilus, the malicious file would exploit the thumbnailer with the potential for malicious code execution. It is best to disable these thumbnailer applications unless they are explicitly required. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/thumbnailers\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*disable-all\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)disable-all(\s*=)/#\1disable-all\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/thumbnailers\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/thumbnailers]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*disable-all\\s*=" "${DCONFFILE}" then sed -i "s/\\s*disable-all\\s*=\\s*.*/disable-all=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/thumbnailers\\]|a\\disable-all=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/thumbnailers/disable-all$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/thumbnailers/disable-all$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/thumbnailers/disable-all$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/thumbnailers/disable-all" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_thumbnailers - low_complexity - medium_disruption - no_reboot_needed - unknown_severity - unknown_strategy - name: Disable All GNOME3 Thumbnailers community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/thumbnailers option: disable-all value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_thumbnailers - low_complexity - medium_disruption - no_reboot_needed - unknown_severity - unknown_strategy - name: Prevent user modification of GNOME3 Thumbnailers ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/thumbnailers/disable-all$ line: /org/gnome/desktop/thumbnailers/disable-all create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_thumbnailers - low_complexity - medium_disruption - no_reboot_needed - unknown_severity - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_thumbnailers - low_complexity - medium_disruption - no_reboot_needed - unknown_severity - unknown_strategy GNOME Network Settings GNOME network settings that apply to the graphical interface. Disable WIFI Network Connection Creation in GNOME3 GNOME allows users to create ad-hoc wireless connections through the NetworkManager applet. Wireless connections should be disabled by adding or setting disable-wifi-create to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/nm-applet] disable-wifi-create=true Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/nm-applet/disable-wifi-create After the settings have been set, run dconf update. 3.1.16 Wireless network connections should not be allowed to be configured by general users on a given system as it could open the system to backdoor attacks. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/nm-applet\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*disable-wifi-create\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)disable-wifi-create(\s*=)/#\1disable-wifi-create\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/nm-applet\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/nm-applet]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*disable-wifi-create\\s*=" "${DCONFFILE}" then sed -i "s/\\s*disable-wifi-create\\s*=\\s*.*/disable-wifi-create=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/nm-applet\\]|a\\disable-wifi-create=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/nm-applet/disable-wifi-create$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/nm-applet/disable-wifi-create$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/nm-applet/disable-wifi-create$" /etc/dconf/db/local.d/ then echo "/org/gnome/nm-applet/disable-wifi-create" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_create - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable WiFi Network Connection Creation in GNOME3 community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/nm-applet option: disable-wifi-create value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_create - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 disablement of WiFi ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/nm-applet/disable-wifi-create$ line: /org/gnome/nm-applet/disable-wifi-create create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_create - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_create - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Disable WIFI Network Notification in GNOME3 By default, GNOME disables WIFI notification. This should be permanently set so that users do not connect to a wireless network when the system finds one. While useful for mobile devices, this setting should be disabled for all other systems. To configure the system to disable the WIFI notication, add or set suppress-wireless-networks-available to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/nm-applet] suppress-wireless-networks-available=true Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/nm-applet/suppress-wireless-networks-available After the settings have been set, run dconf update. 3.1.16 Wireless network connections should not be allowed to be configured by general users on a given system as it could open the system to backdoor attacks. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/nm-applet\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*suppress-wireless-networks-available\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)suppress-wireless-networks-available(\s*=)/#\1suppress-wireless-networks-available\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/nm-applet\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/nm-applet]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*suppress-wireless-networks-available\\s*=" "${DCONFFILE}" then sed -i "s/\\s*suppress-wireless-networks-available\\s*=\\s*.*/suppress-wireless-networks-available=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/nm-applet\\]|a\\suppress-wireless-networks-available=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/nm-applet/suppress-wireless-networks-available$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/nm-applet/suppress-wireless-networks-available$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/nm-applet/suppress-wireless-networks-available$" /etc/dconf/db/local.d/ then echo "/org/gnome/nm-applet/suppress-wireless-networks-available" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_notification - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable WiFi Network Notification in GNOME3 community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/nm-applet option: suppress-wireless-networks-available value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_notification - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 disablement of WiFi ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/nm-applet/suppress-wireless-networks-available$ line: /org/gnome/nm-applet/suppress-wireless-networks-available create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_notification - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - dconf_gnome_disable_wifi_notification - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy GNOME Remote Access Settings GNOME remote access settings that apply to the graphical interface. Require Credential Prompting for Remote Access in GNOME3 By default, GNOME does not require credentials when using Vino for remote access. To configure the system to require remote credentials, add or set authentication-methods to ['vnc'] in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/Vino] authentication-methods=['vnc'] Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/Vino/authentication-methods After the settings have been set, run dconf update. 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) Username and password prompting is required for remote access. Otherwise, non-authorized and nefarious users can access the system freely. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/Vino\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*authentication-methods\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)authentication-methods(\s*=)/#\1authentication-methods\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/Vino\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/Vino]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "['vnc']")" if grep -q "^\\s*authentication-methods\\s*=" "${DCONFFILE}" then sed -i "s/\\s*authentication-methods\\s*=\\s*.*/authentication-methods=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/Vino\\]|a\\authentication-methods=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/Vino/authentication-methods$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/Vino/authentication-methods$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/Vino/authentication-methods$" /etc/dconf/db/local.d/ then echo "/org/gnome/Vino/authentication-methods" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - dconf_gnome_remote_access_credential_prompt - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Require Credential Prompting for Remote Access in GNOME3 community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/Vino option: authentication-methods value: '[''vnc'']' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - dconf_gnome_remote_access_credential_prompt - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 Credential Prompting for Remote Access ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/Vino/authentication-methods$ line: /org/gnome/Vino/authentication-methods create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - dconf_gnome_remote_access_credential_prompt - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - dconf_gnome_remote_access_credential_prompt - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Require Encryption for Remote Access in GNOME3 By default, GNOME requires encryption when using Vino for remote access. To prevent remote access encryption from being disabled, add or set require-encryption to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/Vino] require-encryption=true Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/Vino/require-encryption After the settings have been set, run dconf update. 1 11 12 13 15 16 18 20 3 4 6 9 BAI03.08 BAI07.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS03.01 3.1.13 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 7.6 A.12.1.1 A.12.1.2 A.12.1.4 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-6(a) AC-17(a) AC-17(2) DE.AE-1 PR.DS-7 PR.IP-1 SRG-OS-000480-GPOS-00227 Open X displays allow an attacker to capture keystrokes and to execute commands remotely. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/Vino\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*require-encryption\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)require-encryption(\s*=)/#\1require-encryption\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/Vino\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/Vino]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*require-encryption\\s*=" "${DCONFFILE}" then sed -i "s/\\s*require-encryption\\s*=\\s*.*/require-encryption=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/Vino\\]|a\\require-encryption=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/Vino/require-encryption$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/Vino/require-encryption$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/Vino/require-encryption$" /etc/dconf/db/local.d/ then echo "/org/gnome/Vino/require-encryption" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.13 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - dconf_gnome_remote_access_encryption - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Require Encryption for Remote Access in GNOME3 community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/Vino option: require-encryption value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - dconf_gnome_remote_access_encryption - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME3 Encryption for Remote Access ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/Vino/require-encryption$ line: /org/gnome/Vino/require-encryption create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - dconf_gnome_remote_access_encryption - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - dconf_gnome_remote_access_encryption - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Configure GNOME Screen Locking In the default GNOME3 desktop, the screen can be locked by selecting the user name in the far right corner of the main panel and selecting Lock. The following sections detail commands to enforce idle activation of the screensaver, screen locking, a blank-screen screensaver, and an idle activation time. Because users should be trained to lock the screen when they step away from the computer, the automatic locking feature is only meant as a backup. The root account can be screen-locked; however, the root account should never be used to log into an X Windows environment and should only be used to for direct login via console in emergency circumstances. For more information about enforcing preferences in the GNOME3 environment using the DConf configuration system, see http://wiki.gnome.org/dconf and the man page dconf(1). Screensaver Inactivity timeout Choose allowed duration (in seconds) of inactive graphical sessions 600 900 1800 300 900 Screensaver Lock Delay Choose allowed duration (in seconds) after a screensaver becomes active before displaying an authentication prompt 10 5 0 0 Enable GNOME3 Screensaver Idle Activation To activate the screensaver in the GNOME3 desktop after a period of inactivity, add or set idle-activation-enabled to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/screensaver] idle-activation-enabled=true Once the setting has been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/screensaver/idle-activation-enabled After the settings have been set, run dconf update. 1 12 15 16 5.5.5 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) AC-11(a) PR.AC-7 Req-8.1.8 SRG-OS-000029-GPOS-00010 8.2.8 8.2 A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operating system session prior to vacating the vicinity, GNOME desktops can be configured to identify when a user's session has idled and take action to initiate the session lock. Enabling idle activation of the screensaver ensures the screensaver will be activated after the idle delay. Applications requiring continuous, real-time screen display (such as network management products) require the login session does not have administrator rights and the display station is located in a controlled-access area. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/screensaver\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*idle-activation-enabled\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)idle-activation-enabled(\s*=)/#\1idle-activation-enabled\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/screensaver\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/screensaver]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*idle-activation-enabled\\s*=" "${DCONFFILE}" then sed -i "s/\\s*idle-activation-enabled\\s*=\\s*.*/idle-activation-enabled=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/screensaver\\]|a\\idle-activation-enabled=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/screensaver/idle-activation-enabled$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/screensaver/idle-activation-enabled$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/screensaver/idle-activation-enabled$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/screensaver/idle-activation-enabled" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_idle_activation_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Enable GNOME3 Screensaver Idle Activation community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/screensaver option: idle-activation-enabled value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_idle_activation_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME idle-activation-enabled ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/screensaver/idle-activation-enabled$ line: /org/gnome/desktop/screensaver/idle-activation-enabled create: true when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_idle_activation_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_idle_activation_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Ensure Users Cannot Change GNOME3 Screensaver Idle Activation If not already configured, ensure that users cannot change GNOME3 screensaver lock settings by adding /org/gnome/desktop/screensaver/idle-activation-enabled to /etc/dconf/db/local.d/00-security-settings. For example: /org/gnome/desktop/screensaver/idle-activation-enabled After the settings have been set, run dconf update. 1 12 15 16 5.5.5 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) PR.AC-7 Req-8.1.8 SRG-OS-000029-GPOS-00010 A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to logout because of the temporary nature of the absense. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/screensaver/idle-activation-enabled$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/screensaver/idle-activation-enabled$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/screensaver/idle-activation-enabled$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/screensaver/idle-activation-enabled" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - dconf_gnome_screensaver_idle_activation_locked - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME Screensaver idle-activation-enabled ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/screensaver/idle-activation-enabled$ line: /org/gnome/desktop/screensaver/idle-activation-enabled create: true when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - dconf_gnome_screensaver_idle_activation_locked - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - dconf_gnome_screensaver_idle_activation_locked - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Set GNOME3 Screensaver Inactivity Timeout The idle time-out value for inactivity in the GNOME3 desktop is configured via the idle-delay setting must be set under an appropriate configuration file(s) in the /etc/dconf/db/local.d directory and locked in /etc/dconf/db/local.d/locks directory to prevent user modification. For example, to configure the system for a 15 minute delay, add the following to /etc/dconf/db/local.d/00-security-settings: [org/gnome/desktop/session] idle-delay=uint32 900 1 12 15 16 5.5.5 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-11(a) CM-6(a) PR.AC-7 Req-8.1.8 SRG-OS-000029-GPOS-00010 SRG-OS-000031-GPOS-00012 1.8.3 8.2.8 8.2 A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operating system session prior to vacating the vicinity, GNOME3 can be configured to identify when a user's session has idled and take action to initiate a session lock. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then inactivity_timeout_value='' # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/session\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*idle-delay\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)idle-delay(\s*=)/#\1idle-delay\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/session\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/session]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "uint32 ${inactivity_timeout_value}")" if grep -q "^\\s*idle-delay\\s*=" "${DCONFFILE}" then sed -i "s/\\s*idle-delay\\s*=\\s*.*/idle-delay=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/session\\]|a\\idle-delay=${escaped_value}" "${DCONFFILE}" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_idle_delay - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: XCCDF Value inactivity_timeout_value # promote to variable set_fact: inactivity_timeout_value: !!str tags: - always - name: Set GNOME3 Screensaver Inactivity Timeout community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/session option: idle-delay value: uint32 {{ inactivity_timeout_value }} create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_idle_delay - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_idle_delay - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Set GNOME3 Screensaver Lock Delay After Activation Period To activate the locking delay of the screensaver in the GNOME3 desktop when the screensaver is activated, add or set lock-delay to uint32 in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/screensaver] lock-delay=uint32 After the settings have been set, run dconf update. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-11(a) CM-6(a) PR.AC-7 Req-8.1.8 SRG-OS-000029-GPOS-00010 SRG-OS-000031-GPOS-00012 1.8.3 8.2.8 8.2 A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to logout because of the temporary nature of the absense. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then var_screensaver_lock_delay='' # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/screensaver\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*lock-delay\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)lock-delay(\s*=)/#\1lock-delay\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/screensaver\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/screensaver]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "uint32 ${var_screensaver_lock_delay}")" if grep -q "^\\s*lock-delay\\s*=" "${DCONFFILE}" then sed -i "s/\\s*lock-delay\\s*=\\s*.*/lock-delay=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/screensaver\\]|a\\lock-delay=${escaped_value}" "${DCONFFILE}" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_delay - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: XCCDF Value var_screensaver_lock_delay # promote to variable set_fact: var_screensaver_lock_delay: !!str tags: - always - name: Set GNOME3 Screensaver Lock Delay After Activation Period community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/screensaver option: lock-delay value: uint32 {{ var_screensaver_lock_delay }} create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_delay - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-AC-11(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_delay - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Enable GNOME3 Screensaver Lock After Idle Period To activate locking of the screensaver in the GNOME3 desktop when it is activated, add or set lock-enabled to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/screensaver] lock-enabled=true Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/screensaver/lock-enabled After the settings have been set, run dconf update. 1 12 15 16 5.5.5 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) PR.AC-7 Req-8.1.8 SRG-OS-000028-GPOS-00009 SRG-OS-000030-GPOS-00011 8.2.8 8.2 A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to logout because of the temporary nature of the absense. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/screensaver\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*lock-enabled\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)lock-enabled(\s*=)/#\1lock-enabled\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/screensaver\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/screensaver]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*lock-enabled\\s*=" "${DCONFFILE}" then sed -i "s/\\s*lock-enabled\\s*=\\s*.*/lock-enabled=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/screensaver\\]|a\\lock-enabled=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/screensaver/lock-enabled$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/screensaver/lock-enabled$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/screensaver/lock-enabled$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/screensaver/lock-enabled" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: - '"gdm" in ansible_facts.packages' - ansible_distribution == 'SLES' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Enable GNOME3 Screensaver Lock After Idle Period community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/screensaver option: lock-enabled value: 'true' create: true no_extra_spaces: true when: - '"gdm" in ansible_facts.packages' - ansible_distribution != 'SLES' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME lock-enabled ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/screensaver/lock-enabled$ line: /org/gnome/desktop/screensaver/lock-enabled create: true when: - '"gdm" in ansible_facts.packages' - ansible_distribution != 'SLES' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Enable GNOME3 Screensaver Lock After Idle Period community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/lockdown option: disable-lock-screen value: 'false' create: true no_extra_spaces: true when: - '"gdm" in ansible_facts.packages' - ansible_distribution == 'SLES' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME disable-lock-screen ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/lockdown/disable-lock-screen$ line: /org/gnome/desktop/lockdown/disable-lock-screen create: true when: - '"gdm" in ansible_facts.packages' - ansible_distribution == 'SLES' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Check GNOME3 screenserver disable-lock-screen false ansible.builtin.command: gsettings get org.gnome.desktop.lockdown disable-lock-screen register: cmd_out when: - '"gdm" in ansible_facts.packages' - ansible_distribution == 'SLES' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Update GNOME3 screenserver disable-lock-screen false ansible.builtin.command: gsettings set org.gnome.desktop.lockdown disable-lock-screen false when: - '"gdm" in ansible_facts.packages' - ansible_distribution == 'SLES' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_lock_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Ensure Users Cannot Change GNOME3 Screensaver Lock After Idle Period If not already configured, ensure that users cannot change GNOME3 screensaver lock settings by adding /org/gnome/desktop/screensaver/lock-enabled to /etc/dconf/db/local.d/locks/00-security-settings. For example: /org/gnome/desktop/screensaver/lock-enabled After the settings have been set, run dconf update. 1 12 15 16 5.5.5 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) PR.AC-7 Req-8.1.8 SRG-OS-000028-GPOS-00009 SRG-OS-000030-GPOS-00011 A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to logout because of the temporary nature of the absense. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/screensaver/lock-enabled$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/screensaver/lock-enabled$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/screensaver/lock-enabled$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/screensaver/lock-enabled" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - dconf_gnome_screensaver_lock_locked - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME Screensaver lock-enabled ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/screensaver/lock-enabled$ line: /org/gnome/desktop/screensaver/lock-enabled create: true when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - dconf_gnome_screensaver_lock_locked - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - dconf_gnome_screensaver_lock_locked - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Implement Blank Screensaver To set the screensaver mode in the GNOME3 desktop to a blank screen, add or set picture-uri to string '' in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/screensaver] picture-uri=string '' Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/screensaver/picture-uri After the settings have been set, run dconf update. 1 12 15 16 5.5.5 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-11(1) CM-6(a) AC-11(1).1 PR.AC-7 Req-8.1.8 SRG-OS-000031-GPOS-00012 8.2.8 8.2 Setting the screensaver mode to blank-only conceals the contents of the display from passersby. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/screensaver\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*picture-uri\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)picture-uri(\s*=)/#\1picture-uri\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/screensaver\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/screensaver]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "string ''")" if grep -q "^\\s*picture-uri\\s*=" "${DCONFFILE}" then sed -i "s/\\s*picture-uri\\s*=\\s*.*/picture-uri=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/screensaver\\]|a\\picture-uri=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/screensaver/picture-uri$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/screensaver/picture-uri$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/screensaver/picture-uri$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/screensaver/picture-uri" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(1) - NIST-800-53-AC-11(1).1 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_mode_blank - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Implement Blank Screensaver community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/screensaver option: picture-uri value: string '' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(1) - NIST-800-53-AC-11(1).1 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_mode_blank - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME picture-uri ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/screensaver/picture-uri$ line: /org/gnome/desktop/screensaver/picture-uri create: true when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(1) - NIST-800-53-AC-11(1).1 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_mode_blank - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - CJIS-5.5.5 - NIST-800-171-3.1.10 - NIST-800-53-AC-11(1) - NIST-800-53-AC-11(1).1 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_screensaver_mode_blank - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Disable Full User Name on Splash Shield By default when the screen is locked, the splash shield will show the user's full name. This should be disabled to prevent casual observers from seeing who has access to the system. This can be disabled by adding or setting show-full-name-in-top-bar to false in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/screensaver] show-full-name-in-top-bar=false Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/screensaver/show-full-name-in-top-bar After the settings have been set, run dconf update. Setting the splash screen to not reveal the logged in user's name conceals who has access to the system from passersby. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/screensaver\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*show-full-name-in-top-bar\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)show-full-name-in-top-bar(\s*=)/#\1show-full-name-in-top-bar\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/screensaver\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/screensaver]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "false")" if grep -q "^\\s*show-full-name-in-top-bar\\s*=" "${DCONFFILE}" then sed -i "s/\\s*show-full-name-in-top-bar\\s*=\\s*.*/show-full-name-in-top-bar=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/screensaver\\]|a\\show-full-name-in-top-bar=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/screensaver/show-full-name-in-top-bar$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/screensaver/show-full-name-in-top-bar$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/screensaver/show-full-name-in-top-bar$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/screensaver/show-full-name-in-top-bar" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - dconf_gnome_screensaver_user_info - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable Full Username on Splash Screen community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/desktop/screensaver option: show-full-name-in-top-bar value: 'false' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_screensaver_user_info - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME show-full-name-in-top-bar ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/screensaver/show-full-name-in-top-bar$ line: /org/gnome/desktop/screensaver/show-full-name-in-top-bar create: true when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_screensaver_user_info - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_screensaver_user_info - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Ensure Users Cannot Change GNOME3 Screensaver Settings If not already configured, ensure that users cannot change GNOME3 screensaver lock settings by adding /org/gnome/desktop/screensaver/lock-delay to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/screensaver/lock-delay After the settings have been set, run dconf update. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) PR.AC-7 SRG-OS-000029-GPOS-00010 SRG-OS-000031-GPOS-00012 A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operating system session prior to vacating the vicinity, GNOME desktops can be configured to identify when a user's session has idled and take action to initiate the session lock. As such, users should not be allowed to change session settings. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/screensaver/lock-delay$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/screensaver/lock-delay$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/screensaver/lock-delay$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/screensaver/lock-delay" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - dconf_gnome_screensaver_user_locks - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME lock-delay ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/screensaver/lock-delay$ line: /org/gnome/desktop/screensaver/lock-delay create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - dconf_gnome_screensaver_user_locks - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - dconf_gnome_screensaver_user_locks - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Ensure Users Cannot Change GNOME3 Session Idle Settings If not already configured, ensure that users cannot change GNOME3 session idle settings by adding /org/gnome/desktop/session/idle-delay to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/session/idle-delay After the settings have been set, run dconf update. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) PR.AC-7 Req-8.1.8 SRG-OS-000029-GPOS-00010 SRG-OS-000031-GPOS-00012 8.2.8 8.2 A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operating system session prior to vacating the vicinity, GNOME desktops can be configured to identify when a user's session has idled and take action to initiate the session lock. As such, users should not be allowed to change session settings. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/session/idle-delay$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/session/idle-delay$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/session/idle-delay$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/session/idle-delay" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_session_idle_user_locks - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME Session idle-delay ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/session/idle-delay$ line: /org/gnome/desktop/session/idle-delay create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_session_idle_user_locks - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - dconf_gnome_session_idle_user_locks - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy GNOME System Settings GNOME provides configuration and functionality to a graphical desktop environment that changes grahical configurations or allow a user to perform actions that users normally would not be able to do in non-graphical mode such as remote access configuration, power policies, Geo-location, etc. Configuring such settings in GNOME will prevent accidential graphical configuration changes by users from taking place. Disable Ctrl-Alt-Del Reboot Key Sequence in GNOME3 By default, GNOME will reboot the system if the Ctrl-Alt-Del key sequence is pressed. To configure the system to ignore the Ctrl-Alt-Del key sequence from the Graphical User Interface (GUI) instead of rebooting the system, add or set logout to [''] in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/settings-daemon/plugins/media-keys] logout=[''] Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/settings-daemon/plugins/media-keys/logout After the settings have been set, run dconf update. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.2 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) CM-7(b) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 A locally logged-in user who presses Ctrl-Alt-Del, when at the console, can reboot the system. If accidentally pressed, as could happen in the case of mixed OS environment, this can create the risk of short-term loss of availability of systems due to unintentional reboot. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/settings-daemon/plugins/media-keys\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*logout\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)logout(\s*=)/#\1logout\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/settings-daemon/plugins/media-keys\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/settings-daemon/plugins/media-keys]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "['']")" if grep -q "^\\s*logout\\s*=" "${DCONFFILE}" then sed -i "s/\\s*logout\\s*=\\s*.*/logout=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/settings-daemon/plugins/media-keys\\]|a\\logout=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/settings-daemon/plugins/media-keys/logout$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/settings-daemon/plugins/media-keys/logout$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/settings-daemon/plugins/media-keys/logout$" /etc/dconf/db/local.d/ then echo "/org/gnome/settings-daemon/plugins/media-keys/logout" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_ctrlaltdel_reboot - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Disable Ctrl-Alt-Del Reboot Key Sequence in GNOME3 community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/settings-daemon/plugins/media-keys option: logout value: '['''']' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_ctrlaltdel_reboot - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME disablement of Ctrl-Alt-Del ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/settings-daemon/plugins/media-keys/logout$ line: /org/gnome/settings-daemon/plugins/media-keys/logout create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_ctrlaltdel_reboot - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - dconf_gnome_disable_ctrlaltdel_reboot - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy Disable Geolocation in GNOME3 GNOME allows the clock and applications to track and access location information. This setting should be disabled as applications should not track system location. To configure the system to disable location tracking, add or set enabled to false in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/system/location] enabled=false To configure the clock to disable location tracking, add or set geolocation to false in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/clocks] geolocation=false Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/system/location/enabled /org/gnome/clocks/geolocation After the settings have been set, run dconf update. Power settings should not be enabled on systems that are not mobile devices. Enabling power settings on non-mobile devices could have unintended processing consequences on standard systems. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/system/location\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*enabled\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)enabled(\s*=)/#\1enabled\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/system/location\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/system/location]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "false")" if grep -q "^\\s*enabled\\s*=" "${DCONFFILE}" then sed -i "s/\\s*enabled\\s*=\\s*.*/enabled=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/system/location\\]|a\\enabled=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/clocks\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*geolocation\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)geolocation(\s*=)/#\1geolocation\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/clocks\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/clocks]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "false")" if grep -q "^\\s*geolocation\\s*=" "${DCONFFILE}" then sed -i "s/\\s*geolocation\\s*=\\s*.*/geolocation=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/clocks\\]|a\\geolocation=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/system/location/enabled$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/system/location/enabled$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/system/location/enabled$" /etc/dconf/db/local.d/ then echo "/org/gnome/system/location/enabled" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/clocks/geolocation$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/clocks/geolocation$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/clocks/geolocation$" /etc/dconf/db/local.d/ then echo "/org/gnome/clocks/geolocation" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - dconf_gnome_disable_geolocation - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable Geolocation in GNOME3 - location tracking community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/system/location option: enabled value: 'false' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_disable_geolocation - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Disable Geolocation in GNOME3 - clock location tracking community.general.ini_file: dest: /etc/dconf/db/local.d/00-security-settings section: org/gnome/clocks option: gelocation value: 'false' create: true when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_disable_geolocation - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME geolocation - location tracking ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/system/location/enabled$ line: /org/gnome/system/location/enabled create: true when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_disable_geolocation - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME geolocation - clock location tracking ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/clocks/geolocation$ line: /org/gnome/clocks/geolocation create: true when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_disable_geolocation - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - dconf_gnome_disable_geolocation - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Disable Power Settings in GNOME3 By default, GNOME enables a power profile designed for mobile devices with battery usage. While useful for mobile devices, this setting should be disabled for all other systems. To configure the system to disable the power setting, add or set active to false in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/settings-daemon/plugins/power] active=false Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/settings-daemon/plugins/power After the settings have been set, run dconf update. Power settings should not be enabled on systems that are not mobile devices. Enabling power settings on non-mobile devices could have unintended processing consequences on standard systems. Disable User Administration in GNOME3 By default, GNOME will allow all users to have some administratrion capability. This should be disabled so that non-administrative users are not making configuration changes. To configure the system to disable user administration capability in the Graphical User Interface (GUI), add or set user-administration-disabled to true in /etc/dconf/db/local.d/00-security-settings. For example: [org/gnome/desktop/lockdown] user-administration-disabled=true Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/desktop/lockdown/user-administration-disabled After the settings have been set, run dconf update. 3.1.5 Allowing all users to have some administratrive capabilities to the system through the Graphical User Interface (GUI) when they would not have them otherwise could allow unintended configuration changes as well as a nefarious user the capability to make system changes such as adding new accounts, etc. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/desktop/lockdown\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/local.d/00-security-settings" DBDIR="/etc/dconf/db/local.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*user-administration-disabled\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)user-administration-disabled(\s*=)/#\1user-administration-disabled\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/desktop/lockdown\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/desktop/lockdown]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*user-administration-disabled\\s*=" "${DCONFFILE}" then sed -i "s/\\s*user-administration-disabled\\s*=\\s*.*/user-administration-disabled=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/desktop/lockdown\\]|a\\user-administration-disabled=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/desktop/lockdown/user-administration-disabled$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|local.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/local.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/desktop/lockdown/user-administration-disabled$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/desktop/lockdown/user-administration-disabled$" /etc/dconf/db/local.d/ then echo "/org/gnome/desktop/lockdown/user-administration-disabled" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Detect if user-administration-disabled can be found on /etc/dconf/db/local.d/ ansible.builtin.find: path: /etc/dconf/db/local.d/ contains: ^\s*user-administration-disabled register: dconf_gnome_disable_user_admin_config_files when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Configure user-administration-disabled - default file community.general.ini_file: dest: /etc/dconf/db/local.d//00-security-settings section: org/gnome/desktop/lockdown option: user-administration-disabled value: 'true' create: true when: - '"gdm" in ansible_facts.packages' - dconf_gnome_disable_user_admin_config_files is defined and dconf_gnome_disable_user_admin_config_files.matched == 0 tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Configure user-administration-disabled - existing files community.general.ini_file: dest: '{{ item.path }}' section: org/gnome/desktop/lockdown option: user-administration-disabled value: 'true' create: true with_items: '{{ dconf_gnome_disable_user_admin_config_files.files }}' when: - '"gdm" in ansible_facts.packages' - dconf_gnome_disable_user_admin_config_files is defined and dconf_gnome_disable_user_admin_config_files.matched > 0 tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Detect if lock for user-administration-disabled can be found on /etc/dconf/db/local.d/ ansible.builtin.find: path: /etc/dconf/db/local.d/locks contains: ^\s*user-administration-disabled register: dconf_gnome_disable_user_admin_lock_files when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Prevent user modification user-administration-disabled - default file ansible.builtin.lineinfile: path: /etc/dconf/db/local.d/locks/00-security-settings-lock regexp: ^/org/gnome/desktop/lockdown/user-administration-disabled$ line: /org/gnome/desktop/lockdown/user-administration-disabled create: true when: - '"gdm" in ansible_facts.packages' - dconf_gnome_disable_user_admin_lock_files is defined and dconf_gnome_disable_user_admin_lock_files.matched == 0 tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Prevent user modification user-administration-disabled - existing files ansible.builtin.lineinfile: path: '{{ item.path }}' regexp: ^/org/gnome/desktop/lockdown/user-administration-disabled$ line: /org/gnome/desktop/lockdown/user-administration-disabled create: true with_items: '{{ dconf_gnome_disable_user_admin_lock_files.files }}' when: - '"gdm" in ansible_facts.packages' - dconf_gnome_disable_user_admin_lock_files is defined and dconf_gnome_disable_user_admin_lock_files.matched > 0 tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Dconf Update - user-administration-disabled ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.5 - dconf_gnome_disable_user_admin - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy Sudo Sudo, which stands for "su 'do'", provides the ability to delegate authority to certain users, groups of users, or system administrators. When configured for system users and/or groups, Sudo can allow a user or group to execute privileged commands that normally only root is allowed to execute. For more information on Sudo and addition Sudo configuration options, see https://www.sudo.ws. Sudo - logfile value Specify the sudo logfile to use. The default value used here matches the example location from CIS, which uses /var/log/sudo.log. /var/log/sudo.log /var/log/sudo.log Sudo - timestamp_timeout value Defines the number of minutes that can elapse before sudo will ask for a passwd again. If set to a value less than 0 the user's time stamp will never expire. Defining 0 means always prompt for a password. The default timeout value is 5 minutes. 5 0 1 2 3 5 15 Install sudo Package The sudo package can be installed with the following command: $ sudo dnf install sudo 1382 1384 1386 CM-6(a) FMT_MOF_EXT.1 SRG-OS-000324-GPOS-00125 R33 5.2.1 2.2.6 2.2 sudo is a program designed to allow a system administrator to give limited root privileges to users and log root activity. The basic philosophy is to give as few privileges as possible but still allow system users to get their work done. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "sudo" ; then dnf install -y "sudo" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_sudo_installed - name: Ensure sudo is installed ansible.builtin.package: name: sudo state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_sudo_installed include install_sudo class install_sudo { package { 'sudo': ensure => 'installed', } } package --add=sudo [[packages]] name = "sudo" version = "*" package install sudo dnf install sudo Ensure Privileged Escalated Commands Cannot Execute Other Commands - sudo NOEXEC The sudo NOEXEC tag, when specified, prevents user executed commands from executing other commands, like a shell for example. This should be enabled by making sure that the NOEXEC tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/. R39 Restricting the capability of sudo allowed commands to execute sub-commands prevents users from running programs with privileges they wouldn't have otherwise. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if /usr/sbin/visudo -qcf /etc/sudoers; then cp /etc/sudoers /etc/sudoers.bak if ! grep -P '^[\s]*Defaults\b[^!\n]*\bnoexec.*$' /etc/sudoers; then # sudoers file doesn't define Option noexec echo "Defaults noexec" >> /etc/sudoers fi # Check validity of sudoers and cleanup bak if /usr/sbin/visudo -qcf /etc/sudoers; then rm -f /etc/sudoers.bak else echo "Fail to validate remediated /etc/sudoers, reverting to original file." mv /etc/sudoers.bak /etc/sudoers false fi else echo "Skipping remediation, /etc/sudoers failed to validate" false fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sudo_add_noexec - name: Ensure noexec is enabled in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^[\s]*Defaults.*\bnoexec\b.*$ line: Defaults noexec validate: /usr/sbin/visudo -cf %s when: '"kernel" in ansible_facts.packages' tags: - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sudo_add_noexec Ensure Only Users Logged In To Real tty Can Execute Sudo - sudo requiretty The sudo requiretty tag, when specified, will only execute sudo commands from users logged in to a real tty. This should be enabled by making sure that the requiretty tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/. R39 Restricting the use cases in which a user is allowed to execute sudo commands reduces the attack surface. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if /usr/sbin/visudo -qcf /etc/sudoers; then cp /etc/sudoers /etc/sudoers.bak if ! grep -P '^[\s]*Defaults\b[^!\n]*\brequiretty.*$' /etc/sudoers; then # sudoers file doesn't define Option requiretty echo "Defaults requiretty" >> /etc/sudoers fi # Check validity of sudoers and cleanup bak if /usr/sbin/visudo -qcf /etc/sudoers; then rm -f /etc/sudoers.bak else echo "Fail to validate remediated /etc/sudoers, reverting to original file." mv /etc/sudoers.bak /etc/sudoers false fi else echo "Skipping remediation, /etc/sudoers failed to validate" false fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_add_requiretty - name: Ensure requiretty is enabled in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^[\s]*Defaults.*\brequiretty\b.*$ line: Defaults requiretty validate: /usr/sbin/visudo -cf %s when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_add_requiretty Ensure Only Users Logged In To Real tty Can Execute Sudo - sudo use_pty The sudo use_pty tag, when specified, will only execute sudo commands from users logged in to a real tty. This should be enabled by making sure that the use_pty tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/. Req-10.2.5 R39 5.2.2 2.2.6 2.2 Requiring that sudo commands be run in a pseudo-terminal can prevent an attacker from retaining access to the user's terminal after the main program has finished executing. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q sudo; }; then if /usr/sbin/visudo -qcf /etc/sudoers; then cp /etc/sudoers /etc/sudoers.bak if ! grep -P '^[\s]*Defaults\b[^!\n]*\buse_pty.*$' /etc/sudoers; then # sudoers file doesn't define Option use_pty echo "Defaults use_pty" >> /etc/sudoers fi # Check validity of sudoers and cleanup bak if /usr/sbin/visudo -qcf /etc/sudoers; then rm -f /etc/sudoers.bak else echo "Fail to validate remediated /etc/sudoers, reverting to original file." mv /etc/sudoers.bak /etc/sudoers false fi else echo "Skipping remediation, /etc/sudoers failed to validate" false fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-10.2.5 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_add_use_pty - name: Ensure use_pty is enabled in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^[\s]*Defaults.*\buse_pty\b.*$ line: Defaults use_pty validate: /usr/sbin/visudo -cf %s when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - PCI-DSS-Req-10.2.5 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_add_use_pty Ensure Sudo Logfile Exists - sudo logfile A custom log sudo file can be configured with the 'logfile' tag. This rule configures a sudo custom logfile at the default location suggested by CIS, which uses /var/log/sudo.log. Req-10.2.5 5.2.3 2.2.6 2.2 A sudo log file simplifies auditing of sudo commands. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q sudo; }; then var_sudo_logfile='' if /usr/sbin/visudo -qcf /etc/sudoers; then cp /etc/sudoers /etc/sudoers.bak if ! grep -P '^[\s]*Defaults\b[^!\n]*\blogfile\s*=\s*(?:"?([^",\s]+)"?).*$' /etc/sudoers; then # sudoers file doesn't define Option logfile echo "Defaults logfile=${var_sudo_logfile}" >> /etc/sudoers else # sudoers file defines Option logfile, remediate if appropriate value is not set if ! grep -P "^[\s]*Defaults.*\blogfile=${var_sudo_logfile}\b.*$" /etc/sudoers; then escaped_variable=${var_sudo_logfile//$'/'/$'\/'} sed -Ei "s/(^[\s]*Defaults.*\blogfile=)[-]?.+(\b.*$)/\1$escaped_variable\2/" /etc/sudoers fi fi # Check validity of sudoers and cleanup bak if /usr/sbin/visudo -qcf /etc/sudoers; then rm -f /etc/sudoers.bak else echo "Fail to validate remediated /etc/sudoers, reverting to original file." mv /etc/sudoers.bak /etc/sudoers false fi else echo "Skipping remediation, /etc/sudoers failed to validate" false fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-10.2.5 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sudo_custom_logfile - name: XCCDF Value var_sudo_logfile # promote to variable set_fact: var_sudo_logfile: !!str tags: - always - name: Ensure logfile is enabled with the appropriate value in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^[\s]*Defaults\s(.*)\blogfile=[-]?.+\b(.*)$ line: Defaults \1logfile={{ var_sudo_logfile }}\2 validate: /usr/sbin/visudo -cf %s backrefs: true register: edit_sudoers_logfile_option when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - PCI-DSS-Req-10.2.5 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sudo_custom_logfile - name: Enable logfile option with appropriate value in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers line: Defaults logfile={{ var_sudo_logfile }} validate: /usr/sbin/visudo -cf %s when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' - edit_sudoers_logfile_option is defined and not edit_sudoers_logfile_option.changed tags: - PCI-DSS-Req-10.2.5 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sudo_custom_logfile Ensure Users Re-Authenticate for Privilege Escalation - sudo !authenticate The sudo !authenticate option, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the !authenticate option does not exist in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/. 1 12 15 16 5 DSS05.04 DSS05.10 DSS06.03 DSS06.10 4.3.3.5.1 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-11 CM-6(a) PR.AC-1 PR.AC-7 SRG-OS-000373-GPOS-00156 SRG-OS-000373-GPOS-00157 SRG-OS-000373-GPOS-00158 Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then for f in /etc/sudoers /etc/sudoers.d/* ; do if [ ! -e "$f" ] ; then continue fi matching_list=$(grep -P '^(?!#).*[\s]+\!authenticate.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do # comment out "!authenticate" matches to preserve user data sed -i "s|^${entry}$|# &|g" $f done <<< "$matching_list" /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_remove_no_authenticate - name: Find /etc/sudoers.d/ files ansible.builtin.find: paths: - /etc/sudoers.d/ register: sudoers when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_remove_no_authenticate - name: Remove lines containing !authenticate from sudoers files ansible.builtin.replace: regexp: (^(?!#).*[\s]+\!authenticate.*$) replace: '# \g<1>' path: '{{ item.path }}' validate: /usr/sbin/visudo -cf %s with_items: - path: /etc/sudoers - '{{ sudoers.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_remove_no_authenticate Ensure Users Re-Authenticate for Privilege Escalation - sudo NOPASSWD The sudo NOPASSWD tag, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the NOPASSWD tag does not exist in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/. 1 12 15 16 5 DSS05.04 DSS05.10 DSS06.03 DSS06.10 4.3.3.5.1 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-11 CM-6(a) PR.AC-1 PR.AC-7 SRG-OS-000373-GPOS-00156 SRG-OS-000373-GPOS-00157 SRG-OS-000373-GPOS-00158 Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then for f in /etc/sudoers /etc/sudoers.d/* ; do if [ ! -e "$f" ] ; then continue fi matching_list=$(grep -P '^(?!#).*[\s]+NOPASSWD[\s]*\:.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do # comment out "NOPASSWD" matches to preserve user data sed -i "s|^${entry}$|# &|g" $f done <<< "$matching_list" /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_remove_nopasswd - name: Find /etc/sudoers.d/ files ansible.builtin.find: paths: - /etc/sudoers.d/ register: sudoers when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_remove_nopasswd - name: Remove lines containing NOPASSWD from sudoers files ansible.builtin.replace: regexp: (^(?!#).*[\s]+NOPASSWD[\s]*\:.*$) replace: '# \g<1>' path: '{{ item.path }}' validate: /usr/sbin/visudo -cf %s with_items: - path: /etc/sudoers - '{{ sudoers.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_remove_nopasswd Ensure Users Re-Authenticate for Privilege Escalation - sudo The sudo NOPASSWD and !authenticate option, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that NOPASSWD and/or !authenticate do not exist in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/." 1 12 15 16 5 DSS05.04 DSS05.10 DSS06.03 DSS06.10 4.3.3.5.1 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-11 CM-6(a) PR.AC-1 PR.AC-7 SRG-OS-000373-GPOS-00156 5.2.4 5.2.5 2.2.6 2.2 Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then for f in /etc/sudoers /etc/sudoers.d/* ; do if [ ! -e "$f" ] ; then continue fi matching_list=$(grep -P '^(?!#).*[\s]+NOPASSWD[\s]*\:.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do # comment out "NOPASSWD" matches to preserve user data sed -i "s|^${entry}$|# &|g" $f done <<< "$matching_list" /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo" fi done for f in /etc/sudoers /etc/sudoers.d/* ; do if [ ! -e "$f" ] ; then continue fi matching_list=$(grep -P '^(?!#).*[\s]+\!authenticate.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do # comment out "!authenticate" matches to preserve user data sed -i "s|^${entry}$|# &|g" $f done <<< "$matching_list" /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_authentication - name: Find /etc/sudoers.d/ files ansible.builtin.find: paths: - /etc/sudoers.d/ register: sudoers when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_authentication - name: Remove lines containing NOPASSWD from sudoers files ansible.builtin.replace: regexp: (^(?!#).*[\s]+NOPASSWD[\s]*\:.*$) replace: '# \g<1>' path: '{{ item.path }}' validate: /usr/sbin/visudo -cf %s with_items: - path: /etc/sudoers - '{{ sudoers.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_authentication - name: Find /etc/sudoers.d/ files ansible.builtin.find: paths: - /etc/sudoers.d/ register: sudoers when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_authentication - name: Remove lines containing !authenticate from sudoers files ansible.builtin.replace: regexp: (^(?!#).*[\s]+\!authenticate.*$) replace: '# \g<1>' path: '{{ item.path }}' validate: /usr/sbin/visudo -cf %s with_items: - path: /etc/sudoers - '{{ sudoers.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_authentication Require Re-Authentication When Using the sudo Command The sudo timestamp_timeout tag sets the amount of time sudo password prompt waits. The default timestamp_timeout value is 5 minutes. The timestamp_timeout should be configured by making sure that the timestamp_timeout tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/. If the value is set to an integer less than 0, the user's time stamp will not expire and the user will not have to re-authenticate for privileged actions until the user's session is terminated. IA-11 SRG-OS-000373-GPOS-00156 SRG-OS-000373-GPOS-00157 SRG-OS-000373-GPOS-00158 5.2.6 2.2.6 2.2 Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q sudo; }; then var_sudo_timestamp_timeout='' if grep -Px '^[\s]*Defaults.*timestamp_timeout[\s]*=.*' /etc/sudoers.d/*; then find /etc/sudoers.d/ -type f -exec sed -Ei "/^[[:blank:]]*Defaults.*timestamp_timeout[[:blank:]]*=.*/d" {} \; fi if /usr/sbin/visudo -qcf /etc/sudoers; then cp /etc/sudoers /etc/sudoers.bak if ! grep -P '^[\s]*Defaults.*timestamp_timeout[\s]*=[\s]*[-]?\w+.*$' /etc/sudoers; then # sudoers file doesn't define Option timestamp_timeout echo "Defaults timestamp_timeout=${var_sudo_timestamp_timeout}" >> /etc/sudoers else # sudoers file defines Option timestamp_timeout, remediate wrong values if present if grep -qP "^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=[\s]*(?!${var_sudo_timestamp_timeout}\b)[-]?\w+\b.*$" /etc/sudoers; then sed -Ei "s/(^[[:blank:]]*Defaults.*timestamp_timeout[[:blank:]]*=)[[:blank:]]*[-]?\w+(.*$)/\1${var_sudo_timestamp_timeout}\2/" /etc/sudoers fi fi # Check validity of sudoers and cleanup bak if /usr/sbin/visudo -qcf /etc/sudoers; then rm -f /etc/sudoers.bak else echo "Fail to validate remediated /etc/sudoers, reverting to original file." mv /etc/sudoers.bak /etc/sudoers false fi else echo "Skipping remediation, /etc/sudoers failed to validate" false fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_reauthentication - name: XCCDF Value var_sudo_timestamp_timeout # promote to variable set_fact: var_sudo_timestamp_timeout: !!str tags: - always - name: Require Re-Authentication When Using the sudo Command - Find /etc/sudoers.d/* files containing 'Defaults timestamp_timeout' ansible.builtin.find: path: /etc/sudoers.d patterns: '*' contains: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=.* register: sudoers_d_defaults_timestamp_timeout when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_reauthentication - name: Require Re-Authentication When Using the sudo Command - Remove 'Defaults timestamp_timeout' from /etc/sudoers.d/* files ansible.builtin.lineinfile: path: '{{ item.path }}' regexp: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=.* state: absent with_items: '{{ sudoers_d_defaults_timestamp_timeout.files }}' when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_reauthentication - name: Require Re-Authentication When Using the sudo Command - Ensure timestamp_timeout has the appropriate value in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^[\s]*Defaults\s(.*)\btimestamp_timeout[\s]*=[\s]*[-]?\w+\b(.*)$ line: Defaults \1timestamp_timeout={{ var_sudo_timestamp_timeout }}\2 validate: /usr/sbin/visudo -cf %s backrefs: true register: edit_sudoers_timestamp_timeout_option when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_reauthentication - name: Require Re-Authentication When Using the sudo Command - Enable timestamp_timeout option with correct value in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers line: Defaults timestamp_timeout={{ var_sudo_timestamp_timeout }} validate: /usr/sbin/visudo -cf %s when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' - | edit_sudoers_timestamp_timeout_option is defined and not edit_sudoers_timestamp_timeout_option.changed tags: - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_reauthentication - name: Require Re-Authentication When Using the sudo Command - Remove timestamp_timeout wrong values in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=[\s]*(?!{{ var_sudo_timestamp_timeout }}\b)[-]?\w+\b.*$ state: absent validate: /usr/sbin/visudo -cf %s when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-IA-11 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudo_require_reauthentication Only the VDSM User Can Use sudo NOPASSWD The sudo NOPASSWD tag, when specified, allows a user to execute commands using sudo without having to authenticate. Only the vdsm user should have this capability in any sudo configuration snippets in /etc/sudoers.d/. Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate. Ensure sudo only includes the default configuration directory Administrators can configure authorized sudo users via drop-in files, and it is possible to include other directories and configuration files from the file currently being parsed. Make sure that /etc/sudoers only includes drop-in configuration files from /etc/sudoers.d, or that no drop-in file is included. Either the /etc/sudoers should contain only one #includedir directive pointing to /etc/sudoers.d, and no file in /etc/sudoers.d/ should include other files or directories; Or the /etc/sudoers should not contain any #include, @include, #includedir or @includedir directives. Note that the '#' character doesn't denote a comment in the configuration file. SRG-OS-000480-GPOS-00227 Some sudo configurtion options allow users to run programs without re-authenticating. Use of these configuration options makes it easier for one compromised accound to be used to compromise other accounts. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then sudoers_config_file="/etc/sudoers" sudoers_config_dir="/etc/sudoers.d" sudoers_includedir_count=$(grep -c "#includedir" "$sudoers_config_file") if [ "$sudoers_includedir_count" -gt 1 ]; then sed -i "/#includedir/d" "$sudoers_config_file" echo "#includedir /etc/sudoers.d" >> "$sudoers_config_file" elif [ "$sudoers_includedir_count" -eq 0 ]; then echo "#includedir /etc/sudoers.d" >> "$sudoers_config_file" else if ! grep -q "^#includedir /etc/sudoers.d" "$sudoers_config_file"; then sed -i "s|^#includedir.*|#includedir /etc/sudoers.d|g" "$sudoers_config_file" fi fi sed -Ei "/^#include\s/d; /^@includedir\s/d" "$sudoers_config_file" if grep -Pr "^[#@]include(dir)?\s" "$sudoers_config_dir" ; then sed -Ei "/^[#@]include(dir)?\s/d" "$sudoers_config_dir"/* fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^#includedir.*$ state: absent check_mode: true changed_when: false register: dupes when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir - name: Deduplicate values from /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^#includedir.*$ state: absent when: - '"kernel" in ansible_facts.packages' - dupes.found is defined and dupes.found > 1 tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir - name: Insert correct line into /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^#includedir.*$ line: '#includedir /etc/sudoers.d' state: present when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir - name: Ensure sudoers doesn't include other non-default file ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^[#@]include[\s]+.*$ state: absent when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir - name: Ensure sudoers doesn't have non-default includedir ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^@includedir[\s]+.*$ state: absent when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir - name: Find out if /etc/sudoers.d/* files contain file or directory includes ansible.builtin.find: path: /etc/sudoers.d patterns: '*' contains: ^[#@]include(dir)?\s.*$ register: sudoers_d_includes when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir - name: Remove found occurrences of file and directory includes from /etc/sudoers.d/* files ansible.builtin.lineinfile: path: '{{ item.path }}' regexp: ^[#@]include(dir)?\s.*$ state: absent with_items: '{{ sudoers_d_includes.files }}' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sudoers_default_includedir Explicit arguments in sudo specifications All commands in the sudoers file must strictly specify the arguments allowed to be used for a given user. If the command is supposed to be executed only without arguments, pass "" as an argument in the corresponding user specification. This rule doesn't come with a remediation, as absence of arguments in the user spec doesn't mean that the command is intended to be executed with no arguments. The rule can produce false findings when an argument contains a comma - sudoers syntax allows comma escaping using backslash, but the check doesn't support that. For example, root ALL=(ALL) echo 1\,2 allows root to execute echo 1,2, but the check would interpret it as two commands echo 1\ and 2. R43 Any argument can modify quite significantly the behavior of a program, whether regarding the realized operation (read, write, delete, etc.) or accessed resources (path in a file system tree). To avoid any possibility of misuse of a command by a user, the ambiguities must be removed at the level of its specification. For example, on some systems, the kernel messages are only accessible by root. If a user nevertheless must have the privileges to read them, the argument of the dmesg command has to be restricted in order to prevent the user from flushing the buffer through the -c option: user ALL = dmesg "" Don't define allowed commands in sudoers by means of exclusion Policies applied by sudo through the sudoers file should not involve negation. Each user specification in the sudoers file contains a comma-delimited list of command specifications. The definition can make use glob patterns, as well as of negations. Indirect definition of those commands by means of exclusion of a set of commands is trivial to bypass, so it is not allowed to use such constructs. This rule doesn't come with a remediation, as negations indicate design issues with the sudoers user specifications design. Just removing negations doesn't increase the security - you typically have to rethink the definition of allowed commands to fix the issue. R42 Specifying access right using negation is inefficient and can be easily circumvented. For example, it is expected that a specification like # To avoid absolutely , this rule can be easily circumvented! user ALL = ALL ,!/ bin/sh prevents the execution of the shell but that’s not the case: just copy the binary /bin/sh to a different name to make it executable again through the rule keyword ALL. Don't target root user in the sudoers file The targeted users of a user specification should be, as much as possible, non privileged users (i.e.: non-root). User specifications have to explicitly list the runas spec (i.e. the list of target users that can be impersonated), and ALL or root should not be used. This rule doesn't come with a remediation, as the exact requirement allows exceptions, and removing lines from the sudoers file can make the system non-administrable. R40 It is common that the command to be executed does not require superuser rights (editing a file whose the owner is not root, sending a signal to an unprivileged process,etc.). In order to limit any attempt of privilege escalation through a command, it is better to apply normal user rights. Ensure invoking users password for privilege escalation when using sudo The sudoers security policy requires that users authenticate themselves before they can use sudo. When sudoers requires authentication, it validates the invoking user's credentials. The expected output for: sudo cvtsudoers -f sudoers /etc/sudoers | grep -E '^Defaults !?(rootpw|targetpw|runaspw)$' Defaults !targetpw Defaults !rootpw Defaults !runaspw or if cvtsudoers not supported: sudo find /etc/sudoers /etc/sudoers.d \( \! -name '*~' -a \! -name '*.*' \) -exec grep -E --with-filename '^[[:blank:]]*Defaults[[:blank:]](.*[[:blank:]])?!?\b(rootpw|targetpw|runaspw)' -- {} \; /etc/sudoers:Defaults !targetpw /etc/sudoers:Defaults !rootpw /etc/sudoers:Defaults !runaspw CM-6(b) CM-6.1(iv) SRG-OS-000480-GPOS-00227 If the rootpw, targetpw, or runaspw flags are defined and not disabled, by default the operating system will prompt the invoking user for the "root" user password. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q sudo; }; then if grep -x '^Defaults targetpw$' /etc/sudoers; then sed -i "/Defaults targetpw/d" /etc/sudoers \; fi if grep -x '^Defaults targetpw$' /etc/sudoers.d/*; then find /etc/sudoers.d/ -type f -exec sed -i "/Defaults targetpw/d" {} \; fi if grep -x '^Defaults rootpw$' /etc/sudoers; then sed -i "/Defaults rootpw/d" /etc/sudoers \; fi if grep -x '^Defaults rootpw$' /etc/sudoers.d/*; then find /etc/sudoers.d/ -type f -exec sed -i "/Defaults rootpw/d" {} \; fi if grep -x '^Defaults runaspw$' /etc/sudoers; then sed -i "/Defaults runaspw/d" /etc/sudoers \; fi if grep -x '^Defaults runaspw$' /etc/sudoers.d/*; then find /etc/sudoers.d/ -type f -exec sed -i "/Defaults runaspw/d" {} \; fi if [ -e "/etc/sudoers" ] ; then LC_ALL=C sed -i "/Defaults !targetpw/d" "/etc/sudoers" else touch "/etc/sudoers" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/sudoers" cp "/etc/sudoers" "/etc/sudoers.bak" # Insert at the end of the file printf '%s\n' "Defaults !targetpw" >> "/etc/sudoers" # Clean up after ourselves. rm "/etc/sudoers.bak" if [ -e "/etc/sudoers" ] ; then LC_ALL=C sed -i "/Defaults !rootpw/d" "/etc/sudoers" else touch "/etc/sudoers" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/sudoers" cp "/etc/sudoers" "/etc/sudoers.bak" # Insert at the end of the file printf '%s\n' "Defaults !rootpw" >> "/etc/sudoers" # Clean up after ourselves. rm "/etc/sudoers.bak" if [ -e "/etc/sudoers" ] ; then LC_ALL=C sed -i "/Defaults !runaspw/d" "/etc/sudoers" else touch "/etc/sudoers" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/sudoers" cp "/etc/sudoers" "/etc/sudoers.bak" # Insert at the end of the file printf '%s\n' "Defaults !runaspw" >> "/etc/sudoers" # Clean up after ourselves. rm "/etc/sudoers.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Find out if /etc/sudoers.d/* files contain Defaults targetpw to be deduplicated ansible.builtin.find: path: /etc/sudoers.d patterns: '*' contains: ^Defaults targetpw$ register: sudoers_d_defaults when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Remove found occurrences of Defaults targetpw from /etc/sudoers.d/* files ansible.builtin.lineinfile: path: '{{ item.path }}' regexp: ^Defaults targetpw$ state: absent with_items: '{{ sudoers_d_defaults.files }}' when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Find out if /etc/sudoers.d/* files contain Defaults rootpw to be deduplicated ansible.builtin.find: path: /etc/sudoers.d patterns: '*' contains: ^Defaults rootpw$ register: sudoers_d_defaults when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Remove found occurrences of Defaults rootpw from /etc/sudoers.d/* files ansible.builtin.lineinfile: path: '{{ item.path }}' regexp: ^Defaults rootpw$ state: absent with_items: '{{ sudoers_d_defaults.files }}' when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Find out if /etc/sudoers.d/* files contain Defaults runaspw to be deduplicated ansible.builtin.find: path: /etc/sudoers.d patterns: '*' contains: ^Defaults runaspw$ register: sudoers_d_defaults when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Remove found occurrences of Defaults runaspw from /etc/sudoers.d/* files ansible.builtin.lineinfile: path: '{{ item.path }}' regexp: ^Defaults runaspw$ state: absent with_items: '{{ sudoers_d_defaults.files }}' when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Remove any ocurrences of Defaults targetpw in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^Defaults targetpw$ validate: /usr/sbin/visudo -cf %s state: absent register: sudoers_file_defaults when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Remove any ocurrences of Defaults rootpw in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^Defaults rootpw$ validate: /usr/sbin/visudo -cf %s state: absent register: sudoers_file_defaults when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Remove any ocurrences of Defaults runaspw in /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers regexp: ^Defaults runaspw$ validate: /usr/sbin/visudo -cf %s state: absent register: sudoers_file_defaults when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !targetpw$ state: absent check_mode: true changed_when: false register: dupes when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Deduplicate values from /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !targetpw$ state: absent when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' - dupes.found is defined and dupes.found > 1 tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Insert correct line into /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !targetpw$ line: Defaults !targetpw state: present when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !rootpw$ state: absent check_mode: true changed_when: false register: dupes when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Deduplicate values from /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !rootpw$ state: absent when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' - dupes.found is defined and dupes.found > 1 tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Insert correct line into /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !rootpw$ line: Defaults !rootpw state: present when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !runaspw$ state: absent check_mode: true changed_when: false register: dupes when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Deduplicate values from /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !runaspw$ state: absent when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' - dupes.found is defined and dupes.found > 1 tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd - name: Insert correct line into /etc/sudoers ansible.builtin.lineinfile: path: /etc/sudoers create: false regexp: ^Defaults !runaspw$ line: Defaults !runaspw state: present when: - '"kernel" in ansible_facts.packages' - '"sudo" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sudoers_validate_passwd System Tooling / Utilities The following checks evaluate the system for recommended base packages -- both for installation and removal. Install binutils Package The binutils package can be installed with the following command: $ sudo dnf install binutils binutils is a collection of binary utilities required for foundational system operator activities, such as ld, nm, objcopy and readelf. if ! rpm -q --quiet "binutils" ; then dnf install -y "binutils" fi - name: Ensure binutils is installed ansible.builtin.package: name: binutils state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_binutils_installed include install_binutils class install_binutils { package { 'binutils': ensure => 'installed', } } package --add=binutils [[packages]] name = "binutils" version = "*" package install binutils dnf install binutils Install cryptsetup Package The cryptsetup package can be installed with the following command: $ sudo dnf install cryptsetup 3.5.1.2 3.5.1 3.5 LUKS is the upcoming standard for Linux hard disk encryption. By providing a standard on-disk format, it does not only facilitate compatibility among distributions, but also provide secure management of multiple user passwords. In contrast to existing solution, LUKS stores all necessary setup information in the partition header, enabling the user to transport or migrate their data seamlessly. LUKS for dm-crypt is implemented in cryptsetup. if ! rpm -q --quiet "cryptsetup" ; then dnf install -y "cryptsetup" fi - name: Ensure cryptsetup is installed ansible.builtin.package: name: cryptsetup state: present tags: - PCI-DSSv4-3.5 - PCI-DSSv4-3.5.1 - PCI-DSSv4-3.5.1.2 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_cryptsetup-luks_installed include install_cryptsetup class install_cryptsetup { package { 'cryptsetup': ensure => 'installed', } } package --add=cryptsetup [[packages]] name = "cryptsetup" version = "*" package install cryptsetup dnf install cryptsetup Ensure gnutls-utils is installed The gnutls-utils package can be installed with the following command: $ sudo dnf install gnutls-utils FIA_X509_EXT.1 FIA_X509_EXT.1.1 FIA_X509_EXT.2 SRG-OS-000480-GPOS-00227 GnuTLS is a secure communications library implementing the SSL, TLS and DTLS protocols and technologies around them. It provides a simple C language application programming interface (API) to access the secure communications protocols as well as APIs to parse and write X.509, PKCS #12, OpenPGP and other required structures. This package contains command line TLS client and server and certificate manipulation tools. if ! rpm -q --quiet "gnutls-utils" ; then dnf install -y "gnutls-utils" fi - name: Ensure gnutls-utils is installed ansible.builtin.package: name: gnutls-utils state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_gnutls-utils_installed include install_gnutls-utils class install_gnutls-utils { package { 'gnutls-utils': ensure => 'installed', } } package --add=gnutls-utils [[packages]] name = "gnutls-utils" version = "*" package install gnutls-utils dnf install gnutls-utils Install libcap-ng-utils Package The libcap-ng-utils package can be installed with the following command: $ sudo dnf install libcap-ng-utils SRG-OS-000445-GPOS-00199 libcap-ng-utils contains applications to analyze the posix posix capabilities of all the programs running on a system. libcap-ng-utils also lets system operators set the file system based capabilities. if ! rpm -q --quiet "libcap-ng-utils" ; then dnf install -y "libcap-ng-utils" fi - name: Ensure libcap-ng-utils is installed ansible.builtin.package: name: libcap-ng-utils state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_libcap-ng-utils_installed include install_libcap-ng-utils class install_libcap-ng-utils { package { 'libcap-ng-utils': ensure => 'installed', } } package --add=libcap-ng-utils [[packages]] name = "libcap-ng-utils" version = "*" package install libcap-ng-utils dnf install libcap-ng-utils Ensure nss-tools is installed The nss-tools package can be installed with the following command: $ sudo dnf install nss-tools FMT_SMF_EXT.1 SRG-OS-000480-GPOS-00227 Network Security Services (NSS) is a set of libraries designed to support cross-platform development of security-enabled client and server applications. Install the nss-tools package to install command-line tools to manipulate the NSS certificate and key database. if ! rpm -q --quiet "nss-tools" ; then dnf install -y "nss-tools" fi - name: Ensure nss-tools is installed ansible.builtin.package: name: nss-tools state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_nss-tools_installed include install_nss-tools class install_nss-tools { package { 'nss-tools': ensure => 'installed', } } package --add=nss-tools [[packages]] name = "nss-tools" version = "*" package install nss-tools dnf install nss-tools Install openscap-scanner Package The openscap-scanner package can be installed with the following command: $ sudo dnf install openscap-scanner AGD_PRE.1 AGD_OPE.1 SRG-OS-000480-GPOS-00227 SRG-OS-000191-GPOS-00080 openscap-scanner contains the oscap command line tool. This tool is a configuration and vulnerability scanner, capable of performing compliance checking using SCAP content. if ! rpm -q --quiet "openscap-scanner" ; then dnf install -y "openscap-scanner" fi - name: Ensure openscap-scanner is installed ansible.builtin.package: name: openscap-scanner state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_openscap-scanner_installed include install_openscap-scanner class install_openscap-scanner { package { 'openscap-scanner': ensure => 'installed', } } package --add=openscap-scanner [[packages]] name = "openscap-scanner" version = "*" package install openscap-scanner dnf install openscap-scanner Install rear Package The rear package can be installed with the following command: $ sudo dnf install rear rear contains the Relax-and-Recover (ReaR) utility. ReaR produces a bootable image of a system and restores from backup using this image. # Remediation is applicable only in certain platforms if ! ( ( ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) && grep -qP "^ID=[\"']?ol[\"']?$" "/etc/os-release" && { real="$(grep -P "^VERSION_ID=[\"']?[\w.]+[\"']?$" /etc/os-release | sed "s/^VERSION_ID=[\"']\?\([^\"']\+\)[\"']\?$/\1/")"; expected="9.0"; printf "%s\n%s" "$expected" "$real" | sort -VC; } ) || ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) && grep -qP "^ID=[\"']?rhel[\"']?$" "/etc/os-release" && { real="$(grep -P "^VERSION_ID=[\"']?[\w.]+[\"']?$" /etc/os-release | sed "s/^VERSION_ID=[\"']\?\([^\"']\+\)[\"']\?$/\1/")"; expected="9.0"; printf "%s\n%s" "$expected" "$real" | sort -VC; } ) || ( grep -qP "^ID=[\"']?rhel[\"']?$" "/etc/os-release" && { real="$(grep -P "^VERSION_ID=[\"']?[\w.]+[\"']?$" /etc/os-release | sed "s/^VERSION_ID=[\"']\?\([^\"']\+\)[\"']\?$/\1/")"; expected="8.4"; printf "%s\n%s" "$real" "$expected" | sort -VC; } && ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) ) ) ); then if ! rpm -q --quiet "rear" ; then dnf install -y "rear" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Ensure rear is installed ansible.builtin.package: name: rear state: present when: not ( ( ( ansible_architecture == "aarch64" and ansible_distribution == 'OracleLinux' and ansible_distribution_version is version('9.0', '>=') ) or ( ansible_architecture == "aarch64" and ansible_distribution == 'RedHat' and ansible_distribution_version is version('9.0', '>=') ) or ( ansible_distribution == 'RedHat' and ansible_distribution_version is version('8.4', '<=') and ansible_architecture == "s390x" ) ) ) tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_rear_installed include install_rear class install_rear { package { 'rear': ensure => 'installed', } } package --add=rear [[packages]] name = "rear" version = "*" package install rear dnf install rear Install rng-tools Package The rng-tools package can be installed with the following command: $ sudo dnf install rng-tools SRG-OS-000480-GPOS-00227 rng-tools provides hardware random number generator tools, such as those used in the formation of x509/PKI certificates. # Remediation is applicable only in certain platforms if ( ! ( [ "$(sysctl -a | grep -c 'fips_enabled.*1')" -eq 1 ] ) && rpm --quiet -q kernel ); then if ! rpm -q --quiet "rng-tools" ; then dnf install -y "rng-tools" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_rng-tools_installed - name: Ensure rng-tools is installed ansible.builtin.package: name: rng-tools state: present when: ( "kernel" in ansible_facts.packages ) tags: - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_rng-tools_installed include install_rng-tools class install_rng-tools { package { 'rng-tools': ensure => 'installed', } } package --add=rng-tools [[packages]] name = "rng-tools" version = "*" package install rng-tools dnf install rng-tools Install scap-security-guide Package The scap-security-guide package can be installed with the following command: $ sudo dnf install scap-security-guide AGD_PRE.1 AGD_OPE.1 SRG-OS-000480-GPOS-00227 The scap-security-guide package provides a guide for configuration of the system from the final system's security point of view. The guidance is specified in the Security Content Automation Protocol (SCAP) format and constitutes a catalog of practical hardening advice, linked to government requirements where applicable. The SCAP Security Guide project bridges the gap between generalized policy requirements and specific implementation guidelines. A system administrator can use the oscap CLI tool from the openscap-scanner package, or the SCAP Workbench GUI tool from the scap-workbench package, to verify that the system conforms to provided guidelines. Refer to the scap-security-guide(8) manual page for futher information. if ! rpm -q --quiet "scap-security-guide" ; then dnf install -y "scap-security-guide" fi - name: Ensure scap-security-guide is installed ansible.builtin.package: name: scap-security-guide state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_scap-security-guide_installed include install_scap-security-guide class install_scap-security-guide { package { 'scap-security-guide': ensure => 'installed', } } package --add=scap-security-guide [[packages]] name = "scap-security-guide" version = "*" package install scap-security-guide dnf install scap-security-guide Install tar Package The tar package can be installed with the following command: $ sudo dnf install tar The GNU tar program saves many files together into one archive and can restore individual files (or all of the files) from the archive. tar includes multivolume support, automatic archive compression/decompression, the the ability to perform incremental and full backups. If if ! rpm -q --quiet "tar" ; then dnf install -y "tar" fi - name: Ensure tar is installed ansible.builtin.package: name: tar state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_tar_installed include install_tar class install_tar { package { 'tar': ensure => 'installed', } } package --add=tar [[packages]] name = "tar" version = "*" package install tar dnf install tar Install vim Package The vim-enhanced package can be installed with the following command: $ sudo dnf install vim-enhanced Vim (Vi IMproved) is an almost compatible version of the UNIX editor vi. if ! rpm -q --quiet "vim-enhanced" ; then dnf install -y "vim-enhanced" fi - name: Ensure vim-enhanced is installed ansible.builtin.package: name: vim-enhanced state: present tags: - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_vim_installed include install_vim-enhanced class install_vim-enhanced { package { 'vim-enhanced': ensure => 'installed', } } package --add=vim-enhanced [[packages]] name = "vim-enhanced" version = "*" package install vim-enhanced dnf install vim-enhanced Uninstall abrt-addon-ccpp Package The abrt-addon-ccpp package can be removed with the following command: $ sudo dnf remove abrt-addon-ccpp SRG-OS-000095-GPOS-00049 abrt-addon-ccpp contains hooks for C/C++ crashed programs and abrt's C/C++ analyzer plugin. # CAUTION: This remediation script will remove abrt-addon-ccpp # from the system, and may remove any packages # that depend on abrt-addon-ccpp. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "abrt-addon-ccpp" ; then dnf remove -y --noautoremove "abrt-addon-ccpp" fi - name: 'Uninstall abrt-addon-ccpp Package: Ensure abrt-addon-ccpp is removed' ansible.builtin.package: name: abrt-addon-ccpp state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_abrt-addon-ccpp_removed include remove_abrt-addon-ccpp class remove_abrt-addon-ccpp { package { 'abrt-addon-ccpp': ensure => 'purged', } } package --remove=abrt-addon-ccpp package remove abrt-addon-ccpp dnf remove abrt-addon-ccpp Uninstall abrt-addon-kerneloops Package The abrt-addon-kerneloops package can be removed with the following command: $ sudo dnf remove abrt-addon-kerneloops SRG-OS-000095-GPOS-00049 abrt-addon-kerneloops contains plugins for collecting kernel crash information and reporter plugin which sends this information to a specified server, usually to kerneloops.org. # CAUTION: This remediation script will remove abrt-addon-kerneloops # from the system, and may remove any packages # that depend on abrt-addon-kerneloops. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "abrt-addon-kerneloops" ; then dnf remove -y --noautoremove "abrt-addon-kerneloops" fi - name: 'Uninstall abrt-addon-kerneloops Package: Ensure abrt-addon-kerneloops is removed' ansible.builtin.package: name: abrt-addon-kerneloops state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_abrt-addon-kerneloops_removed include remove_abrt-addon-kerneloops class remove_abrt-addon-kerneloops { package { 'abrt-addon-kerneloops': ensure => 'purged', } } package --remove=abrt-addon-kerneloops package remove abrt-addon-kerneloops dnf remove abrt-addon-kerneloops Uninstall abrt-cli Package The abrt-cli package can be removed with the following command: $ sudo dnf remove abrt-cli SRG-OS-000095-GPOS-00049 abrt-cli contains a command line client for controlling abrt daemon over sockets. # CAUTION: This remediation script will remove abrt-cli # from the system, and may remove any packages # that depend on abrt-cli. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "abrt-cli" ; then dnf remove -y --noautoremove "abrt-cli" fi - name: 'Uninstall abrt-cli Package: Ensure abrt-cli is removed' ansible.builtin.package: name: abrt-cli state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_abrt-cli_removed include remove_abrt-cli class remove_abrt-cli { package { 'abrt-cli': ensure => 'purged', } } package --remove=abrt-cli package remove abrt-cli dnf remove abrt-cli Uninstall abrt-plugin-logger Package The abrt-plugin-logger package can be removed with the following command: $ sudo dnf remove abrt-plugin-logger SRG-OS-000095-GPOS-00049 abrt-plugin-logger is an ABRT plugin which writes a report to a specified file. # CAUTION: This remediation script will remove abrt-plugin-logger # from the system, and may remove any packages # that depend on abrt-plugin-logger. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "abrt-plugin-logger" ; then dnf remove -y --noautoremove "abrt-plugin-logger" fi - name: 'Uninstall abrt-plugin-logger Package: Ensure abrt-plugin-logger is removed' ansible.builtin.package: name: abrt-plugin-logger state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_abrt-plugin-logger_removed include remove_abrt-plugin-logger class remove_abrt-plugin-logger { package { 'abrt-plugin-logger': ensure => 'purged', } } package --remove=abrt-plugin-logger package remove abrt-plugin-logger dnf remove abrt-plugin-logger Uninstall abrt-plugin-rhtsupport Package The abrt-plugin-rhtsupport package can be removed with the following command: $ sudo dnf remove abrt-plugin-rhtsupport SRG-OS-000095-GPOS-00049 abrt-plugin-rhtsupport is a ABRT plugin to report bugs into the Red Hat Support system. # CAUTION: This remediation script will remove abrt-plugin-rhtsupport # from the system, and may remove any packages # that depend on abrt-plugin-rhtsupport. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "abrt-plugin-rhtsupport" ; then dnf remove -y --noautoremove "abrt-plugin-rhtsupport" fi - name: 'Uninstall abrt-plugin-rhtsupport Package: Ensure abrt-plugin-rhtsupport is removed' ansible.builtin.package: name: abrt-plugin-rhtsupport state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_abrt-plugin-rhtsupport_removed include remove_abrt-plugin-rhtsupport class remove_abrt-plugin-rhtsupport { package { 'abrt-plugin-rhtsupport': ensure => 'purged', } } package --remove=abrt-plugin-rhtsupport package remove abrt-plugin-rhtsupport dnf remove abrt-plugin-rhtsupport Uninstall abrt-plugin-sosreport Package The abrt-plugin-sosreport package can be removed with the following command: $ sudo dnf remove abrt-plugin-sosreport SRG-OS-000095-GPOS-00049 abrt-plugin-sosreport provides a plugin to include an sosreport in an ABRT report. # CAUTION: This remediation script will remove abrt-plugin-sosreport # from the system, and may remove any packages # that depend on abrt-plugin-sosreport. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "abrt-plugin-sosreport" ; then dnf remove -y --noautoremove "abrt-plugin-sosreport" fi - name: 'Uninstall abrt-plugin-sosreport Package: Ensure abrt-plugin-sosreport is removed' ansible.builtin.package: name: abrt-plugin-sosreport state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_abrt-plugin-sosreport_removed include remove_abrt-plugin-sosreport class remove_abrt-plugin-sosreport { package { 'abrt-plugin-sosreport': ensure => 'purged', } } package --remove=abrt-plugin-sosreport package remove abrt-plugin-sosreport dnf remove abrt-plugin-sosreport Uninstall geolite2-city Package The geolite2-city package can be removed with the following command: $ sudo dnf remove geolite2-city geolite2-city is part of the GeoLite2 database packages, offering geolocation databases and tooling. # CAUTION: This remediation script will remove geolite2-city # from the system, and may remove any packages # that depend on geolite2-city. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "geolite2-city" ; then dnf remove -y --noautoremove "geolite2-city" fi - name: 'Uninstall geolite2-city Package: Ensure geolite2-city is removed' ansible.builtin.package: name: geolite2-city state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_geolite2-city_removed include remove_geolite2-city class remove_geolite2-city { package { 'geolite2-city': ensure => 'purged', } } package --remove=geolite2-city package remove geolite2-city dnf remove geolite2-city Uninstall geolite2-country Package The geolite2-country package can be removed with the following command: $ sudo dnf remove geolite2-country geolite2-country is part of the GeoLite2 database packages, offering geolocation databases and tooling. # CAUTION: This remediation script will remove geolite2-country # from the system, and may remove any packages # that depend on geolite2-country. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "geolite2-country" ; then dnf remove -y --noautoremove "geolite2-country" fi - name: 'Uninstall geolite2-country Package: Ensure geolite2-country is removed' ansible.builtin.package: name: geolite2-country state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_geolite2-country_removed include remove_geolite2-country class remove_geolite2-country { package { 'geolite2-country': ensure => 'purged', } } package --remove=geolite2-country package remove geolite2-country dnf remove geolite2-country Uninstall gssproxy Package The gssproxy package can be removed with the following command: $ sudo dnf remove gssproxy SRG-OS-000095-GPOS-00049 SRG-OS-000480-GPOS-00227 gssproxy is a proxy for GSS API credential handling. Kerberos relies on some key derivation functions that may not be compatible with some site policies such as FIPS 140. # CAUTION: This remediation script will remove gssproxy # from the system, and may remove any packages # that depend on gssproxy. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "gssproxy" ; then dnf remove -y --noautoremove "gssproxy" fi - name: 'Uninstall gssproxy Package: Ensure gssproxy is removed' ansible.builtin.package: name: gssproxy state: absent tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_gssproxy_removed include remove_gssproxy class remove_gssproxy { package { 'gssproxy': ensure => 'purged', } } package remove gssproxy dnf remove gssproxy Uninstall iprutils Package The iprutils package can be removed with the following command: $ sudo dnf remove iprutils SRG-OS-000095-GPOS-00049 SRG-OS-000480-GPOS-00227 iprutils provides a suite of utlilities to manage and configure SCSI devices supported by the ipr SCSI storage device driver. # CAUTION: This remediation script will remove iprutils # from the system, and may remove any packages # that depend on iprutils. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "iprutils" ; then dnf remove -y --noautoremove "iprutils" fi - name: 'Uninstall iprutils Package: Ensure iprutils is removed' ansible.builtin.package: name: iprutils state: absent tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_iprutils_removed include remove_iprutils class remove_iprutils { package { 'iprutils': ensure => 'purged', } } package --remove=iprutils package remove iprutils dnf remove iprutils Uninstall krb5-workstation Package The krb5-workstation package can be removed with the following command: $ sudo dnf remove krb5-workstation SRG-OS-000095-GPOS-00049 SRG-OS-000120-GPOS-00061 Kerberos is a network authentication system. The krb5-workstation package contains the basic Kerberos programs (kinit, klist, kdestroy, kpasswd). # CAUTION: This remediation script will remove krb5-workstation # from the system, and may remove any packages # that depend on krb5-workstation. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "krb5-workstation" ; then dnf remove -y --noautoremove "krb5-workstation" fi - name: 'Uninstall krb5-workstation Package: Ensure krb5-workstation is removed' ansible.builtin.package: name: krb5-workstation state: absent tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_krb5-workstation_removed include remove_krb5-workstation class remove_krb5-workstation { package { 'krb5-workstation': ensure => 'purged', } } package --remove=krb5-workstation package remove krb5-workstation dnf remove krb5-workstation Uninstall libreport-plugin-logger Package The libreport-plugin-logger package can be removed with the following command: $ sudo dnf remove libreport-plugin-logger SRG-OS-000095-GPOS-00049 libreport-plugin-logger is a ABRT plugin to report bugs into the Red Hat Support system. # CAUTION: This remediation script will remove libreport-plugin-logger # from the system, and may remove any packages # that depend on libreport-plugin-logger. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "libreport-plugin-logger" ; then dnf remove -y --noautoremove "libreport-plugin-logger" fi - name: 'Uninstall libreport-plugin-logger Package: Ensure libreport-plugin-logger is removed' ansible.builtin.package: name: libreport-plugin-logger state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_libreport-plugin-logger_removed include remove_libreport-plugin-logger class remove_libreport-plugin-logger { package { 'libreport-plugin-logger': ensure => 'purged', } } package --remove=libreport-plugin-logger package remove libreport-plugin-logger dnf remove libreport-plugin-logger Uninstall libreport-plugin-rhtsupport Package The libreport-plugin-rhtsupport package can be removed with the following command: $ sudo dnf remove libreport-plugin-rhtsupport SRG-OS-000095-GPOS-00049 libreport-plugin-rhtsupport is a ABRT plugin to report bugs into the Red Hat Support system. # CAUTION: This remediation script will remove libreport-plugin-rhtsupport # from the system, and may remove any packages # that depend on libreport-plugin-rhtsupport. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "libreport-plugin-rhtsupport" ; then dnf remove -y --noautoremove "libreport-plugin-rhtsupport" fi - name: 'Uninstall libreport-plugin-rhtsupport Package: Ensure libreport-plugin-rhtsupport is removed' ansible.builtin.package: name: libreport-plugin-rhtsupport state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_libreport-plugin-rhtsupport_removed include remove_libreport-plugin-rhtsupport class remove_libreport-plugin-rhtsupport { package { 'libreport-plugin-rhtsupport': ensure => 'purged', } } package --remove=libreport-plugin-rhtsupport package remove libreport-plugin-rhtsupport dnf remove libreport-plugin-rhtsupport Uninstall python3-abrt-addon Package The python3-abrt-addon package can be removed with the following command: $ sudo dnf remove python3-abrt-addon SRG-OS-000095-GPOS-00049 python3-abrt-addon contains python hook and python analyzer plugin for handling uncaught exceptions in python programs. # CAUTION: This remediation script will remove python3-abrt-addon # from the system, and may remove any packages # that depend on python3-abrt-addon. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "python3-abrt-addon" ; then dnf remove -y --noautoremove "python3-abrt-addon" fi - name: 'Uninstall python3-abrt-addon Package: Ensure python3-abrt-addon is removed' ansible.builtin.package: name: python3-abrt-addon state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_python3-abrt-addon_removed include remove_python3-abrt-addon class remove_python3-abrt-addon { package { 'python3-abrt-addon': ensure => 'purged', } } package --remove=python3-abrt-addon package remove python3-abrt-addon dnf remove python3-abrt-addon Uninstall tuned Package The tuned package can be removed with the following command: $ sudo dnf remove tuned SRG-OS-000095-GPOS-00049 SRG-OS-000480-GPOS-00227 tuned contains a daemon that tunes the system settings dynamically. It does so by monitoring the usage of several system components periodically. Based on that information, components will then be put into lower or higher power savings modes to adapt to the current usage. # CAUTION: This remediation script will remove tuned # from the system, and may remove any packages # that depend on tuned. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "tuned" ; then dnf remove -y --noautoremove "tuned" fi - name: 'Uninstall tuned Package: Ensure tuned is removed' ansible.builtin.package: name: tuned state: absent tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_tuned_removed include remove_tuned class remove_tuned { package { 'tuned': ensure => 'purged', } } package --remove=tuned package remove tuned dnf remove tuned Updating Software The dnf command line tool is used to install and update software packages. The system also provides a graphical software update tool in the System menu, in the Administration submenu, called Software Update. Fedora systems contain an installed software catalog called the RPM database, which records metadata of installed packages. Consistently using dnf or the graphical Software Update for all software installation allows for insight into the current inventory of installed software on the system. Install dnf-automatic Package The dnf-automatic package can be installed with the following command: $ sudo dnf install dnf-automatic FPT_TUD_EXT.1 FPT_TUD_EXT.2 SRG-OS-000191-GPOS-00080 R61 dnf-automatic is an alternative command line interface (CLI) to dnf upgrade suitable for automatic, regular execution. # Remediation is applicable only in certain platforms if ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ); then if ! rpm -q --quiet "dnf-automatic" ; then dnf install -y "dnf-automatic" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_dnf-automatic_installed - name: Ensure dnf-automatic is installed ansible.builtin.package: name: dnf-automatic state: present when: not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_dnf-automatic_installed include install_dnf-automatic class install_dnf-automatic { package { 'dnf-automatic': ensure => 'installed', } } package --add=dnf-automatic [[packages]] name = "dnf-automatic" version = "*" package install dnf-automatic dnf install dnf-automatic Install GNOME Software The gnome-software package can be installed with the following command: $ sudo dnf install gnome-software The GNOME software package must be installed so that it can be used for software and firmware updates. if ! rpm -q --quiet "gnome-software" ; then dnf install -y "gnome-software" fi - name: Ensure gnome-software is installed ansible.builtin.package: name: gnome-software state: present tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_gnome_software_installed include install_gnome-software class install_gnome-software { package { 'gnome-software': ensure => 'installed', } } package --add=gnome-software [[packages]] name = "gnome-software" version = "*" package install gnome-software dnf install gnome-software Ensure dnf Removes Previous Package Versions dnf should be configured to remove previous software components after new versions have been installed. To configure dnf to remove the previous software components after updating, set the clean_requirements_on_remove to 1 in /etc/dnf/dnf.conf. 18 20 4 APO12.01 APO12.02 APO12.03 APO12.04 BAI03.10 DSS05.01 DSS05.02 3.4.8 4.2.3 4.2.3.12 4.2.3.7 4.2.3.9 A.12.6.1 A.14.2.3 A.16.1.3 A.18.2.2 A.18.2.3 SI-2(6) CM-11(a) CM-11(b) CM-6(a) ID.RA-1 PR.IP-12 SRG-OS-000437-GPOS-00194 Previous versions of software components that are not removed from the information system after updates have been installed may be exploited by some adversaries. # Remediation is applicable only in certain platforms if rpm --quiet -q dnf; then if grep --silent ^clean_requirements_on_remove /etc/dnf/dnf.conf ; then sed -i "s/^clean_requirements_on_remove.*/clean_requirements_on_remove=1/g" /etc/dnf/dnf.conf else echo -e "\n# Set clean_requirements_on_remove to 1 per security requirements" >> /etc/dnf/dnf.conf echo "clean_requirements_on_remove=1" >> /etc/dnf/dnf.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(6) - clean_components_post_updating - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Ensure dnf Removes Previous Package Versions - Ensure DNF Removes Previous Package Versions ansible.builtin.lineinfile: dest: /etc/dnf/dnf.conf regexp: ^#?clean_requirements_on_remove line: clean_requirements_on_remove=1 insertafter: \[main\] create: true when: '"dnf" in ansible_facts.packages' tags: - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(6) - clean_components_post_updating - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy Configure dnf-automatic to Install Available Updates Automatically To ensure that the packages comprising the available updates will be automatically installed by dnf-automatic, set apply_updates to yes under [commands] section in /etc/dnf/automatic.conf. 0940 1144 1467 1472 1483 1493 1494 1495 SI-2(5) CM-6(a) SI-2(c) FMT_SMF_EXT.1 SRG-OS-000805-GPOS-00260 R61 Installing software updates is a fundamental mitigation against the exploitation of publicly-known vulnerabilities. If the most recent security patches and updates are not installed, unauthorized users may take advantage of weaknesses in the unpatched software. The lack of prompt attention to patching could result in a system compromise. The automated installation of updates ensures that recent security patches are applied in a timely manner. # Remediation is applicable only in certain platforms if ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ); then found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/dnf/automatic.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[commands\]([^\n\[]*\n+)+?[[:space:]]*apply_updates" "$f"; then sed -i "s/apply_updates[^(\n)]*/apply_updates=yes/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[commands\]" "$f"; then sed -i "/[[:space:]]*\[commands\]/a apply_updates=yes" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/dnf/automatic.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[commands]\napply_updates=yes" >> "$file" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(5) - NIST-800-53-SI-2(c) - dnf-automatic_apply_updates - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Configure dnf-automatic to Install Available Updates Automatically community.general.ini_file: dest: /etc/dnf/automatic.conf section: commands option: apply_updates value: 'yes' create: true when: not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(5) - NIST-800-53-SI-2(c) - dnf-automatic_apply_updates - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Configure dnf-automatic to Install Only Security Updates To configure dnf-automatic to install only security updates automatically, set upgrade_type to security under [commands] section in /etc/dnf/automatic.conf. SI-2(5) CM-6(a) SI-2(c) SRG-OS-000191-GPOS-00080 R61 By default, dnf-automatic installs all available updates. Reducing the amount of updated packages only to updates that were issued as a part of a security advisory increases the system stability. # Remediation is applicable only in certain platforms if ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ); then found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/dnf/automatic.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[commands\]([^\n\[]*\n+)+?[[:space:]]*upgrade_type" "$f"; then sed -i "s/upgrade_type[^(\n)]*/upgrade_type=security/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[commands\]" "$f"; then sed -i "/[[:space:]]*\[commands\]/a upgrade_type=security" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/dnf/automatic.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[commands]\nupgrade_type=security" >> "$file" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(5) - NIST-800-53-SI-2(c) - dnf-automatic_security_updates_only - low_complexity - low_severity - medium_disruption - no_reboot_needed - unknown_strategy - name: Configure dnf-automatic to Install Only Security Updates community.general.ini_file: dest: /etc/dnf/automatic.conf section: commands option: upgrade_type value: security create: true when: not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(5) - NIST-800-53-SI-2(c) - dnf-automatic_security_updates_only - low_complexity - low_severity - medium_disruption - no_reboot_needed - unknown_strategy Ensure Fedora GPG Key Installed To ensure the system can cryptographically verify base software packages come from Fedora (and to connect to the Fedora Network to receive them), the Fedora GPG key must properly be installed. To install the Fedora GPG key, run one of the commands below, depending on your Fedora vesion: $ sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-38-primary" $ sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-37-primary" 11 2 3 9 5.10.4.1 APO01.06 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS06.02 3.4.8 164.308(a)(1)(ii)(D) 164.312(b) 164.312(c)(1) 164.312(c)(2) 164.312(e)(2)(i) 4.3.4.3.2 4.3.4.3.3 4.3.4.4.4 SR 3.1 SR 3.3 SR 3.4 SR 3.8 SR 7.6 A.11.2.4 A.12.1.2 A.12.2.1 A.12.5.1 A.12.6.2 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 CM-5(3) SI-7 SC-12 SC-12(3) CM-6(a) PR.DS-6 PR.DS-8 PR.IP-1 Req-6.2 Changes to software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. The Fedora GPG key is necessary to cryptographically verify packages are from Fedora." if ! rpm -q --quiet "gpg" ; then dnf install -y "gpg" fi fedora_version=$(grep -oP '[[:digit:]]+' /etc/redhat-release) function get_release_fingerprint { if [ "${fedora_version}" -eq "40" ]; then readonly FEDORA_RELEASE_FINGERPRINT="115DF9AEF857853EE8445D0A0727707EA15B79CC" elif [ "${fedora_version}" -eq "38" ]; then readonly FEDORA_RELEASE_FINGERPRINT="6A51BBABBA3D5467B6171221809A8D7CEB10B464" elif [ "${fedora_version}" -eq "37" ]; then readonly FEDORA_RELEASE_FINGERPRINT="ACB5EE4E831C74BB7C168D27F55AD3FB5323552A" elif [ "${fedora_version}" -eq "39" ]; then readonly FEDORA_RELEASE_FINGERPRINT="E8F23996F23218640CB44CBE75CF5AC418B8E74C" else printf '%s\n' "This Fedora version '$fedora_version' is not supported anymore, please upgrade to a newer version." >&2 return 1 fi } # Location of the key we would like to import (once it's integrity verified) readonly REDHAT_RELEASE_KEY="/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-${fedora_version}-primary" RPM_GPG_DIR_PERMS=$(stat -c %a "$(dirname "$REDHAT_RELEASE_KEY")") function remediate_gpgkey_installed { # Return if there was an issue getting the release fingerprint get_release_fingerprint || return 1 # Verify /etc/pki/rpm-gpg directory permissions are safe if [ "${RPM_GPG_DIR_PERMS}" -le "755" ]; then # If they are safe, try to obtain fingerprints from the key file # (to ensure there won't be e.g. CRC error). readarray -t GPG_OUT < <(gpg --show-keys --with-fingerprint --with-colons "${REDHAT_RELEASE_KEY}" | grep '^fpr' | cut -d ":" -f 10) GPG_RESULT=$? # No CRC error, safe to proceed if [ "${GPG_RESULT}" -eq "0" ]; then echo "${GPG_OUT[*]}" | grep -vE "${FEDORA_RELEASE_FINGERPRINT}" || { # If file doesn't contain any keys with unknown fingerprint, import it rpm --import "${REDHAT_RELEASE_KEY}" } fi fi } remediate_gpgkey_installed Ensure gpgcheck Enabled In Main dnf Configuration The gpgcheck option controls whether RPM packages' signatures are always checked prior to installation. To configure dnf to check package signatures before installing them, ensure the following line appears in /etc/dnf/dnf.conf in the [main] section: gpgcheck=1 11 2 3 9 5.10.4.1 APO01.06 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS06.02 3.4.8 164.308(a)(1)(ii)(D) 164.312(b) 164.312(c)(1) 164.312(c)(2) 164.312(e)(2)(i) 4.3.4.3.2 4.3.4.3.3 4.3.4.4.4 SR 3.1 SR 3.3 SR 3.4 SR 3.8 SR 7.6 A.11.2.4 A.12.1.2 A.12.2.1 A.12.5.1 A.12.6.2 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 CM-5(3) SI-7 SC-12 SC-12(3) CM-6(a) SA-12 SA-12(10) CM-11(a) CM-11(b) PR.DS-6 PR.DS-8 PR.IP-1 FPT_TUD_EXT.1 FPT_TUD_EXT.2 Req-6.2 SRG-OS-000366-GPOS-00153 R59 1.2.1.2 6.3.3 6.3 Changes to any software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization. Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA). # Remediation is applicable only in certain platforms if rpm --quiet -q dnf; then # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^gpgcheck") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^gpgcheck\\>" "/etc/dnf/dnf.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^gpgcheck\\>.*/$escaped_formatted_output/gi" "/etc/dnf/dnf.conf" else if [[ -s "/etc/dnf/dnf.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/dnf/dnf.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/dnf/dnf.conf" fi printf '%s\n' "$formatted_output" >> "/etc/dnf/dnf.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.4.1 - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-5(3) - NIST-800-53-CM-6(a) - NIST-800-53-SA-12 - NIST-800-53-SA-12(10) - NIST-800-53-SC-12 - NIST-800-53-SC-12(3) - NIST-800-53-SI-7 - PCI-DSS-Req-6.2 - PCI-DSSv4-6.3 - PCI-DSSv4-6.3.3 - configure_strategy - ensure_gpgcheck_globally_activated - high_severity - low_complexity - medium_disruption - no_reboot_needed - name: Ensure GPG check is globally activated community.general.ini_file: dest: /etc/dnf/dnf.conf section: main option: gpgcheck value: 1 no_extra_spaces: true create: false when: '"dnf" in ansible_facts.packages' tags: - CJIS-5.10.4.1 - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-5(3) - NIST-800-53-CM-6(a) - NIST-800-53-SA-12 - NIST-800-53-SA-12(10) - NIST-800-53-SC-12 - NIST-800-53-SC-12(3) - NIST-800-53-SI-7 - PCI-DSS-Req-6.2 - PCI-DSSv4-6.3 - PCI-DSSv4-6.3.3 - configure_strategy - ensure_gpgcheck_globally_activated - high_severity - low_complexity - medium_disruption - no_reboot_needed Ensure gpgcheck Enabled for Local Packages dnf should be configured to verify the signature(s) of local packages prior to installation. To configure dnf to verify signatures of local packages, set the localpkg_gpgcheck to 1 in /etc/dnf/dnf.conf. 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.4.8 164.308(a)(1)(ii)(D) 164.312(b) 164.312(c)(1) 164.312(c)(2) 164.312(e)(2)(i) 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-11(a) CM-11(b) CM-6(a) CM-5(3) SA-12 SA-12(10) PR.IP-1 FPT_TUD_EXT.1 FPT_TUD_EXT.2 SRG-OS-000366-GPOS-00153 R59 Changes to any software components can have significant effects to the overall security of the operating system. This requirement ensures the software has not been tampered and has been provided by a trusted vendor. Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization. # Remediation is applicable only in certain platforms if rpm --quiet -q dnf; then # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^localpkg_gpgcheck") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^localpkg_gpgcheck\\>" "/etc/dnf/dnf.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^localpkg_gpgcheck\\>.*/$escaped_formatted_output/gi" "/etc/dnf/dnf.conf" else if [[ -s "/etc/dnf/dnf.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/dnf/dnf.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/dnf/dnf.conf" fi printf '%s\n' "$formatted_output" >> "/etc/dnf/dnf.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-5(3) - NIST-800-53-CM-6(a) - NIST-800-53-SA-12 - NIST-800-53-SA-12(10) - ensure_gpgcheck_local_packages - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy - name: Ensure GPG check Enabled for Local Packages (dnf) block: - name: Check stats of dnf ansible.builtin.stat: path: /etc/dnf/dnf.conf register: pkg - name: Check if config file of dnf is a symlink ansible.builtin.set_fact: pkg_config_file_symlink: '{{ pkg.stat.lnk_target if pkg.stat.lnk_target is match("^/.*") else "/etc/dnf/dnf.conf" | dirname ~ "/" ~ pkg.stat.lnk_target }}' when: pkg.stat.lnk_target is defined - name: Ensure GPG check Enabled for Local Packages (dnf) community.general.ini_file: dest: '{{ pkg_config_file_symlink | default("/etc/dnf/dnf.conf") }}' section: main option: localpkg_gpgcheck value: 1 no_extra_spaces: true create: true when: '"dnf" in ansible_facts.packages' tags: - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-5(3) - NIST-800-53-CM-6(a) - NIST-800-53-SA-12 - NIST-800-53-SA-12(10) - ensure_gpgcheck_local_packages - high_severity - low_complexity - medium_disruption - no_reboot_needed - unknown_strategy Ensure gpgcheck Enabled for All dnf Package Repositories To ensure signature checking is not disabled for any repos, remove any lines from files in /etc/yum.repos.d of the form: gpgcheck=0 11 2 3 9 5.10.4.1 APO01.06 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS06.02 3.4.8 164.308(a)(1)(ii)(D) 164.312(b) 164.312(c)(1) 164.312(c)(2) 164.312(e)(2)(i) 4.3.4.3.2 4.3.4.3.3 4.3.4.4.4 SR 3.1 SR 3.3 SR 3.4 SR 3.8 SR 7.6 A.11.2.4 A.12.1.2 A.12.2.1 A.12.5.1 A.12.6.2 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 CM-5(3) SI-7 SC-12 SC-12(3) CM-6(a) SA-12 SA-12(10) CM-11(a) CM-11(b) PR.DS-6 PR.DS-8 PR.IP-1 FPT_TUD_EXT.1 FPT_TUD_EXT.2 Req-6.2 SRG-OS-000366-GPOS-00153 R59 6.3.3 6.3 Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA)." sed -i 's/gpgcheck\s*=.*/gpgcheck=1/g' /etc/yum.repos.d/* - name: Grep for dnf repo section names ansible.builtin.shell: | set -o pipefail grep -HEr '^\[.+\]' -r /etc/yum.repos.d/ register: repo_grep_results failed_when: repo_grep_results.rc not in [0, 1] changed_when: false tags: - CJIS-5.10.4.1 - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-5(3) - NIST-800-53-CM-6(a) - NIST-800-53-SA-12 - NIST-800-53-SA-12(10) - NIST-800-53-SC-12 - NIST-800-53-SC-12(3) - NIST-800-53-SI-7 - PCI-DSS-Req-6.2 - PCI-DSSv4-6.3 - PCI-DSSv4-6.3.3 - enable_strategy - ensure_gpgcheck_never_disabled - high_severity - low_complexity - medium_disruption - no_reboot_needed - name: Set gpgcheck=1 for each dnf repo community.general.ini_file: path: '{{ item[0] }}' section: '{{ item[1] }}' option: gpgcheck value: '1' no_extra_spaces: true loop: '{{ repo_grep_results.stdout |regex_findall( ''(.+\.repo):\[(.+)\]\n?'' ) if repo_grep_results is not skipped else [] }}' when: repo_grep_results is not skipped tags: - CJIS-5.10.4.1 - NIST-800-171-3.4.8 - NIST-800-53-CM-11(a) - NIST-800-53-CM-11(b) - NIST-800-53-CM-5(3) - NIST-800-53-CM-6(a) - NIST-800-53-SA-12 - NIST-800-53-SA-12(10) - NIST-800-53-SC-12 - NIST-800-53-SC-12(3) - NIST-800-53-SI-7 - PCI-DSS-Req-6.2 - PCI-DSSv4-6.3 - PCI-DSSv4-6.3.3 - enable_strategy - ensure_gpgcheck_never_disabled - high_severity - low_complexity - medium_disruption - no_reboot_needed Ensure gpgcheck Enabled for Repository Metadata Verify the operating system prevents the installation of patches, service packs, device drivers, or operating system components of local packages without verification of the repository metadata. Check that dnf verifies the repository metadata prior to install with the following command. This should be configured by setting repo_gpgcheck to 1 in /etc/dnf/dnf.conf. 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 164.308(a)(1)(ii)(D) 164.312(b) 164.312(c)(1) 164.312(c)(2) 164.312(e)(2)(i) 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-5(3) SI-7 SC-12 SC-12(3) CM-6(a) SA-12 SA-12(10) CM-11(a) CM-11(b) PR.IP-1 SRG-OS-000366-GPOS-00153 Changes to any software components can have significant effects to the overall security of the operating system. This requirement ensures the software has not been tampered and has been provided by a trusted vendor. Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization. Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. The operating system should not have to verify the software again. NOTE: For U.S. Military systems, this requirement does not mandate DoD certificates for this purpose; however, the certificate used to verify the software must be from an approved Certificate Authority. Ensure Software Patches Installed NOTE: U.S. Defense systems are required to be patched within 30 days or sooner as local policy dictates. Fedora does not have a corresponding OVAL CVE Feed. Therefore, this will result in a "not checked" result during a scan. 18 20 4 5.10.4.1 APO12.01 APO12.02 APO12.03 APO12.04 BAI03.10 DSS05.01 DSS05.02 4.2.3 4.2.3.12 4.2.3.7 4.2.3.9 A.12.6.1 A.14.2.3 A.16.1.3 A.18.2.2 A.18.2.3 SI-2(5) SI-2(c) CM-6(a) ID.RA-1 PR.IP-12 FMT_MOF_EXT.1 Req-6.2 SRG-OS-000480-GPOS-00227 R61 6.3.3 6.3 Installing software updates is a fundamental mitigation against the exploitation of publicly-known vulnerabilities. If the most recent security patches and updates are not installed, unauthorized users may take advantage of weaknesses in the unpatched software. The lack of prompt attention to patching could result in a system compromise. - name: Security patches are up to date ansible.builtin.package: name: '*' state: latest tags: - CJIS-5.10.4.1 - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(5) - NIST-800-53-SI-2(c) - PCI-DSS-Req-6.2 - PCI-DSSv4-6.3 - PCI-DSSv4-6.3.3 - high_disruption - low_complexity - medium_severity - patch_strategy - reboot_required - security_patches_up_to_date - skip_ansible_lint Enable dnf-automatic Timer The dnf-automatic timer can be enabled with the following command: $ sudo systemctl enable dnf-automatic.timer SI-2(5) CM-6(a) SI-2(c) FMT_SMF_EXT.1 SRG-OS-000191-GPOS-00080 R61 The dnf-automatic is an alternative command line interface (CLI) to dnf upgrade with specific facilities to make it suitable to be executed automatically and regularly from systemd timers, cron jobs and similar. The tool is controlled by dnf-automatic.timer SystemD timer. # Remediation is applicable only in certain platforms if ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ); then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'dnf-automatic.timer' fi "$SYSTEMCTL_EXEC" enable 'dnf-automatic.timer' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(5) - NIST-800-53-SI-2(c) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - timer_dnf-automatic_enabled - name: Enable timer dnf-automatic block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable timer dnf-automatic ansible.builtin.systemd: name: dnf-automatic.timer enabled: 'yes' state: started when: - '"dnf-automatic" in ansible_facts.packages' when: not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-2(5) - NIST-800-53-SI-2(c) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - timer_dnf-automatic_enabled Account and Access Control In traditional Unix security, if an attacker gains shell access to a certain login account, they can perform any action or access any file to which that account has access. Therefore, making it more difficult for unauthorized people to gain shell access to accounts, particularly to privileged accounts, is a necessary part of securing a system. This section introduces mechanisms for restricting access to accounts under Fedora. Authselect profile Specify the authselect profile to select local minimal minimal sssd Enable authselect Configure user authentication setup to use the authselect tool. If authselect profile is selected, the rule will enable the profile. If the sudo authselect select command returns an error informing that the chosen profile cannot be selected, it is probably because PAM files have already been modified by the administrator. If this is the case, in order to not overwrite the desired changes made by the administrator, the current PAM settings should be investigated before forcing the selection of the chosen authselect profile. 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) AC-3 FIA_UAU.1 FIA_AFL.1 SRG-OS-000480-GPOS-00227 R31 8.3.4 8.3 Authselect is a successor to authconfig. It is a tool to select system authentication and identity sources from a list of supported profiles instead of letting the administrator manually build the PAM stack. That way, it avoids potential breakage of configuration, as it ships several tested profiles that are well tested and supported to solve different use-cases. var_authselect_profile='' authselect current if test "$?" -ne 0; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; }; then authselect select --force "$var_authselect_profile" else authselect select "$var_authselect_profile" fi if test "$?" -ne 0; then if rpm --quiet --verify pam; then authselect select --force "$var_authselect_profile" else echo "authselect is not used but files from the 'pam' package have been altered, so the authselect configuration won't be forced." >&2 fi fi fi - name: XCCDF Value var_authselect_profile # promote to variable set_fact: var_authselect_profile: !!str tags: - always - name: Enable authselect - Check Current authselect Profile ansible.builtin.command: cmd: authselect current register: result_authselect_current changed_when: false failed_when: false tags: - NIST-800-53-AC-3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - configure_strategy - enable_authselect - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Enable authselect - Try to Select an authselect Profile ansible.builtin.command: cmd: authselect select "{{ var_authselect_profile }}" register: result_authselect_select changed_when: result_authselect_select.rc == 0 failed_when: false when: result_authselect_current.rc != 0 tags: - NIST-800-53-AC-3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - configure_strategy - enable_authselect - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Enable authselect - Verify If pam Has Been Altered ansible.builtin.command: cmd: rpm -qV pam register: result_altered_authselect changed_when: false failed_when: false when: - result_authselect_select is not skipped - result_authselect_select.rc != 0 tags: - NIST-800-53-AC-3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - configure_strategy - enable_authselect - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Enable authselect - Informative Message Based on authselect Integrity Check ansible.builtin.assert: that: - result_authselect_current.rc == 0 or result_altered_authselect is skipped or result_altered_authselect.rc == 0 fail_msg: - authselect is not used but files from the 'pam' package have been altered, so the authselect configuration won't be forced. tags: - NIST-800-53-AC-3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - configure_strategy - enable_authselect - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Enable authselect - Force authselect Profile Selection ansible.builtin.command: cmd: authselect select --force "{{ var_authselect_profile }}" when: - result_authselect_current.rc != 0 - result_authselect_select.rc != 0 - result_altered_authselect.rc == 0 tags: - NIST-800-53-AC-3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - configure_strategy - enable_authselect - low_complexity - medium_disruption - medium_severity - no_reboot_needed Warning Banners for System Accesses Each system should expose as little information about itself as possible. System banners, which are typically displayed just before a login prompt, give out information about the service or the host's operating system. This might include the distribution name and the system kernel version, and the particular version of a network service. This information can assist intruders in gaining access to the system as it can reveal whether the system is running vulnerable software. Most network services can be configured to limit what information is displayed. Many organizations implement security policies that require a system banner provide notice of the system's ownership, provide warning to unauthorized users, and remind authorized users of their consent to monitoring. CIS Login Banner Verbiage Enter an appropriate login banner for your organization according to the local policy. Authorized users only. All activity may be monitored and reported. Authorized users only. All activity may be monitored and reported. Login Banner Verbiage Enter an appropriate login banner for your organization. Please note that new lines must be expressed by the '\n' character and special characters like parentheses and quotation marks must be escaped with '\\'. ^(Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$ ^Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ ^(You[\s\n]+are[\s\n]+accessing[\s\n]+a[\s\n]+U\.S\.[\s\n]+Government[\s\n]+\(USG\)[\s\n]+Information[\s\n]+System[\s\n]+\(IS\)[\s\n]+that[\s\n]+is[\s\n]+provided[\s\n]+for[\s\n]+USG\-authorized[\s\n]+use[\s\n]+only\.[\s\n]+By[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+\(which[\s\n]+includes[\s\n]+any[\s\n]+device[\s\n]+attached[\s\n]+to[\s\n]+this[\s\n]+IS\),[\s\n]+you[\s\n]+consent[\s\n]+to[\s\n]+the[\s\n]+following[\s\n]+conditions\:(?:[\n]+|(?:\\n)+)\-The[\s\n]+USG[\s\n]+routinely[\s\n]+intercepts[\s\n]+and[\s\n]+monitors[\s\n]+communications[\s\n]+on[\s\n]+this[\s\n]+IS[\s\n]+for[\s\n]+purposes[\s\n]+including,[\s\n]+but[\s\n]+not[\s\n]+limited[\s\n]+to,[\s\n]+penetration[\s\n]+testing,[\s\n]+COMSEC[\s\n]+monitoring,[\s\n]+network[\s\n]+operations[\s\n]+and[\s\n]+defense,[\s\n]+personnel[\s\n]+misconduct[\s\n]+\(PM\),[\s\n]+law[\s\n]+enforcement[\s\n]+\(LE\),[\s\n]+and[\s\n]+counterintelligence[\s\n]+\(CI\)[\s\n]+investigations\.(?:[\n]+|(?:\\n)+)\-At[\s\n]+any[\s\n]+time,[\s\n]+the[\s\n]+USG[\s\n]+may[\s\n]+inspect[\s\n]+and[\s\n]+seize[\s\n]+data[\s\n]+stored[\s\n]+on[\s\n]+this[\s\n]+IS\.(?:[\n]+|(?:\\n)+)\-Communications[\s\n]+using,[\s\n]+or[\s\n]+data[\s\n]+stored[\s\n]+on,[\s\n]+this[\s\n]+IS[\s\n]+are[\s\n]+not[\s\n]+private,[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+routine[\s\n]+monitoring,[\s\n]+interception,[\s\n]+and[\s\n]+search,[\s\n]+and[\s\n]+may[\s\n]+be[\s\n]+disclosed[\s\n]+or[\s\n]+used[\s\n]+for[\s\n]+any[\s\n]+USG\-authorized[\s\n]+purpose\.(?:[\n]+|(?:\\n)+)\-This[\s\n]+IS[\s\n]+includes[\s\n]+security[\s\n]+measures[\s\n]+\(e\.g\.,[\s\n]+authentication[\s\n]+and[\s\n]+access[\s\n]+controls\)[\s\n]+to[\s\n]+protect[\s\n]+USG[\s\n]+interests\-\-not[\s\n]+for[\s\n]+your[\s\n]+personal[\s\n]+benefit[\s\n]+or[\s\n]+privacy\.(?:[\n]+|(?:\\n)+)\-Notwithstanding[\s\n]+the[\s\n]+above,[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+does[\s\n]+not[\s\n]+constitute[\s\n]+consent[\s\n]+to[\s\n]+PM,[\s\n]+LE[\s\n]+or[\s\n]+CI[\s\n]+investigative[\s\n]+searching[\s\n]+or[\s\n]+monitoring[\s\n]+of[\s\n]+the[\s\n]+content[\s\n]+of[\s\n]+privileged[\s\n]+communications,[\s\n]+or[\s\n]+work[\s\n]+product,[\s\n]+related[\s\n]+to[\s\n]+personal[\s\n]+representation[\s\n]+or[\s\n]+services[\s\n]+by[\s\n]+attorneys,[\s\n]+psychotherapists,[\s\n]+or[\s\n]+clergy,[\s\n]+and[\s\n]+their[\s\n]+assistants\.[\s\n]+Such[\s\n]+communications[\s\n]+and[\s\n]+work[\s\n]+product[\s\n]+are[\s\n]+private[\s\n]+and[\s\n]+confidential\.[\s\n]+See[\s\n]+User[\s\n]+Agreement[\s\n]+for[\s\n]+details\.|I've[\s\n]+read[\s\n]+\&[\s\n]+consent[\s\n]+to[\s\n]+terms[\s\n]+in[\s\n]+IS[\s\n]+user[\s\n]+agreem't\.)$ ^You[\s\n]+are[\s\n]+accessing[\s\n]+a[\s\n]+U\.S\.[\s\n]+Government[\s\n]+\(USG\)[\s\n]+Information[\s\n]+System[\s\n]+\(IS\)[\s\n]+that[\s\n]+is[\s\n]+provided[\s\n]+for[\s\n]+USG\-authorized[\s\n]+use[\s\n]+only\.[\s\n]+By[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+\(which[\s\n]+includes[\s\n]+any[\s\n]+device[\s\n]+attached[\s\n]+to[\s\n]+this[\s\n]+IS\),[\s\n]+you[\s\n]+consent[\s\n]+to[\s\n]+the[\s\n]+following[\s\n]+conditions\:(?:[\n]+|(?:\\n)+)\-The[\s\n]+USG[\s\n]+routinely[\s\n]+intercepts[\s\n]+and[\s\n]+monitors[\s\n]+communications[\s\n]+on[\s\n]+this[\s\n]+IS[\s\n]+for[\s\n]+purposes[\s\n]+including,[\s\n]+but[\s\n]+not[\s\n]+limited[\s\n]+to,[\s\n]+penetration[\s\n]+testing,[\s\n]+COMSEC[\s\n]+monitoring,[\s\n]+network[\s\n]+operations[\s\n]+and[\s\n]+defense,[\s\n]+personnel[\s\n]+misconduct[\s\n]+\(PM\),[\s\n]+law[\s\n]+enforcement[\s\n]+\(LE\),[\s\n]+and[\s\n]+counterintelligence[\s\n]+\(CI\)[\s\n]+investigations\.(?:[\n]+|(?:\\n)+)\-At[\s\n]+any[\s\n]+time,[\s\n]+the[\s\n]+USG[\s\n]+may[\s\n]+inspect[\s\n]+and[\s\n]+seize[\s\n]+data[\s\n]+stored[\s\n]+on[\s\n]+this[\s\n]+IS\.(?:[\n]+|(?:\\n)+)\-Communications[\s\n]+using,[\s\n]+or[\s\n]+data[\s\n]+stored[\s\n]+on,[\s\n]+this[\s\n]+IS[\s\n]+are[\s\n]+not[\s\n]+private,[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+routine[\s\n]+monitoring,[\s\n]+interception,[\s\n]+and[\s\n]+search,[\s\n]+and[\s\n]+may[\s\n]+be[\s\n]+disclosed[\s\n]+or[\s\n]+used[\s\n]+for[\s\n]+any[\s\n]+USG\-authorized[\s\n]+purpose\.(?:[\n]+|(?:\\n)+)\-This[\s\n]+IS[\s\n]+includes[\s\n]+security[\s\n]+measures[\s\n]+\(e\.g\.,[\s\n]+authentication[\s\n]+and[\s\n]+access[\s\n]+controls\)[\s\n]+to[\s\n]+protect[\s\n]+USG[\s\n]+interests\-\-not[\s\n]+for[\s\n]+your[\s\n]+personal[\s\n]+benefit[\s\n]+or[\s\n]+privacy\.(?:[\n]+|(?:\\n)+)\-Notwithstanding[\s\n]+the[\s\n]+above,[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+does[\s\n]+not[\s\n]+constitute[\s\n]+consent[\s\n]+to[\s\n]+PM,[\s\n]+LE[\s\n]+or[\s\n]+CI[\s\n]+investigative[\s\n]+searching[\s\n]+or[\s\n]+monitoring[\s\n]+of[\s\n]+the[\s\n]+content[\s\n]+of[\s\n]+privileged[\s\n]+communications,[\s\n]+or[\s\n]+work[\s\n]+product,[\s\n]+related[\s\n]+to[\s\n]+personal[\s\n]+representation[\s\n]+or[\s\n]+services[\s\n]+by[\s\n]+attorneys,[\s\n]+psychotherapists,[\s\n]+or[\s\n]+clergy,[\s\n]+and[\s\n]+their[\s\n]+assistants\.[\s\n]+Such[\s\n]+communications[\s\n]+and[\s\n]+work[\s\n]+product[\s\n]+are[\s\n]+private[\s\n]+and[\s\n]+confidential\.[\s\n]+See[\s\n]+User[\s\n]+Agreement[\s\n]+for[\s\n]+details\.$ ^I've[\s\n]+read[\s\n]+\&[\s\n]+consent[\s\n]+to[\s\n]+terms[\s\n]+in[\s\n]+IS[\s\n]+user[\s\n]+agreem't\.$ ^Use[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+constitutes[\s\n]+consent[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times\.[\s\n]+This[\s\n]+is[\s\n]+a[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system\.[\s\n]+All[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+and[\s\n]+related[\s\n]+equipment[\s\n]+are[\s\n]+intended[\s\n]+for[\s\n]+the[\s\n]+communication,[\s\n]+transmission,[\s\n]+processing,[\s\n]+and[\s\n]+storage[\s\n]+of[\s\n]+official[\s\n]+U\.S\.[\s\n]+Government[\s\n]+or[\s\n]+other[\s\n]+authorized[\s\n]+information[\s\n]+only\.[\s\n]+All[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times[\s\n]+to[\s\n]+ensure[\s\n]+proper[\s\n]+functioning[\s\n]+of[\s\n]+equipment[\s\n]+and[\s\n]+systems[\s\n]+including[\s\n]+security[\s\n]+devices[\s\n]+and[\s\n]+systems,[\s\n]+to[\s\n]+prevent[\s\n]+unauthorized[\s\n]+use[\s\n]+and[\s\n]+violations[\s\n]+of[\s\n]+statutes[\s\n]+and[\s\n]+security[\s\n]+regulations,[\s\n]+to[\s\n]+deter[\s\n]+criminal[\s\n]+activity,[\s\n]+and[\s\n]+for[\s\n]+other[\s\n]+similar[\s\n]+purposes\.[\s\n]+Any[\s\n]+user[\s\n]+of[\s\n]+a[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+should[\s\n]+be[\s\n]+aware[\s\n]+that[\s\n]+any[\s\n]+information[\s\n]+placed[\s\n]+in[\s\n]+the[\s\n]+system[\s\n]+is[\s\n]+subject[\s\n]+to[\s\n]+monitoring[\s\n]+and[\s\n]+is[\s\n]+not[\s\n]+subject[\s\n]+to[\s\n]+any[\s\n]+expectation[\s\n]+of[\s\n]+privacy\.[\s\n]+If[\s\n]+monitoring[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+reveals[\s\n]+possible[\s\n]+evidence[\s\n]+of[\s\n]+violation[\s\n]+of[\s\n]+criminal[\s\n]+statutes,[\s\n]+this[\s\n]+evidence[\s\n]+and[\s\n]+any[\s\n]+other[\s\n]+related[\s\n]+information,[\s\n]+including[\s\n]+identification[\s\n]+information[\s\n]+about[\s\n]+the[\s\n]+user,[\s\n]+may[\s\n]+be[\s\n]+provided[\s\n]+to[\s\n]+law[\s\n]+enforcement[\s\n]+officials\.[\s\n]+If[\s\n]+monitoring[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+reveals[\s\n]+violations[\s\n]+of[\s\n]+security[\s\n]+regulations[\s\n]+or[\s\n]+unauthorized[\s\n]+use,[\s\n]+employees[\s\n]+who[\s\n]+violate[\s\n]+security[\s\n]+regulations[\s\n]+or[\s\n]+make[\s\n]+unauthorized[\s\n]+use[\s\n]+of[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+appropriate[\s\n]+disciplinary[\s\n]+action\.[\s\n]+Use[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+constitutes[\s\n]+consent[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times\.$ ^\-\-[\s\n]+WARNING[\s\n]+\-\-[\s\n]+This[\s\n]+system[\s\n]+is[\s\n]+for[\s\n]+the[\s\n]+use[\s\n]+of[\s\n]+authorized[\s\n]+users[\s\n]+only\.[\s\n]+Individuals[\s\n]+using[\s\n]+this[\s\n]+computer[\s\n]+system[\s\n]+without[\s\n]+authority[\s\n]+or[\s\n]+in[\s\n]+excess[\s\n]+of[\s\n]+their[\s\n]+authority[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+having[\s\n]+all[\s\n]+their[\s\n]+activities[\s\n]+on[\s\n]+this[\s\n]+system[\s\n]+monitored[\s\n]+and[\s\n]+recorded[\s\n]+by[\s\n]+system[\s\n]+personnel\.[\s\n]+Anyone[\s\n]+using[\s\n]+this[\s\n]+system[\s\n]+expressly[\s\n]+consents[\s\n]+to[\s\n]+such[\s\n]+monitoring[\s\n]+and[\s\n]+is[\s\n]+advised[\s\n]+that[\s\n]+if[\s\n]+such[\s\n]+monitoring[\s\n]+reveals[\s\n]+possible[\s\n]+evidence[\s\n]+of[\s\n]+criminal[\s\n]+activity[\s\n]+system[\s\n]+personal[\s\n]+may[\s\n]+provide[\s\n]+the[\s\n]+evidence[\s\n]+of[\s\n]+such[\s\n]+monitoring[\s\n]+to[\s\n]+law[\s\n]+enforcement[\s\n]+officials\.$ ^Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ MotD Banner Verbiage Enter an appropriate login banner for your organization. Please note that new lines must be expressed by the '\n' character and special characters like parentheses and quotation marks must be escaped with '\\'. ^(Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$ ^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ ^(You[\s\n]+are[\s\n]+accessing[\s\n]+a[\s\n]+U\.S\.[\s\n]+Government[\s\n]+\(USG\)[\s\n]+Information[\s\n]+System[\s\n]+\(IS\)[\s\n]+that[\s\n]+is[\s\n]+provided[\s\n]+for[\s\n]+USG\-authorized[\s\n]+use[\s\n]+only\.[\s\n]+By[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+\(which[\s\n]+includes[\s\n]+any[\s\n]+device[\s\n]+attached[\s\n]+to[\s\n]+this[\s\n]+IS\),[\s\n]+you[\s\n]+consent[\s\n]+to[\s\n]+the[\s\n]+following[\s\n]+conditions\:(?:[\n]+|(?:\\n)+)\-The[\s\n]+USG[\s\n]+routinely[\s\n]+intercepts[\s\n]+and[\s\n]+monitors[\s\n]+communications[\s\n]+on[\s\n]+this[\s\n]+IS[\s\n]+for[\s\n]+purposes[\s\n]+including,[\s\n]+but[\s\n]+not[\s\n]+limited[\s\n]+to,[\s\n]+penetration[\s\n]+testing,[\s\n]+COMSEC[\s\n]+monitoring,[\s\n]+network[\s\n]+operations[\s\n]+and[\s\n]+defense,[\s\n]+personnel[\s\n]+misconduct[\s\n]+\(PM\),[\s\n]+law[\s\n]+enforcement[\s\n]+\(LE\),[\s\n]+and[\s\n]+counterintelligence[\s\n]+\(CI\)[\s\n]+investigations\.(?:[\n]+|(?:\\n)+)\-At[\s\n]+any[\s\n]+time,[\s\n]+the[\s\n]+USG[\s\n]+may[\s\n]+inspect[\s\n]+and[\s\n]+seize[\s\n]+data[\s\n]+stored[\s\n]+on[\s\n]+this[\s\n]+IS\.(?:[\n]+|(?:\\n)+)\-Communications[\s\n]+using,[\s\n]+or[\s\n]+data[\s\n]+stored[\s\n]+on,[\s\n]+this[\s\n]+IS[\s\n]+are[\s\n]+not[\s\n]+private,[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+routine[\s\n]+monitoring,[\s\n]+interception,[\s\n]+and[\s\n]+search,[\s\n]+and[\s\n]+may[\s\n]+be[\s\n]+disclosed[\s\n]+or[\s\n]+used[\s\n]+for[\s\n]+any[\s\n]+USG\-authorized[\s\n]+purpose\.(?:[\n]+|(?:\\n)+)\-This[\s\n]+IS[\s\n]+includes[\s\n]+security[\s\n]+measures[\s\n]+\(e\.g\.,[\s\n]+authentication[\s\n]+and[\s\n]+access[\s\n]+controls\)[\s\n]+to[\s\n]+protect[\s\n]+USG[\s\n]+interests\-\-not[\s\n]+for[\s\n]+your[\s\n]+personal[\s\n]+benefit[\s\n]+or[\s\n]+privacy\.(?:[\n]+|(?:\\n)+)\-Notwithstanding[\s\n]+the[\s\n]+above,[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+does[\s\n]+not[\s\n]+constitute[\s\n]+consent[\s\n]+to[\s\n]+PM,[\s\n]+LE[\s\n]+or[\s\n]+CI[\s\n]+investigative[\s\n]+searching[\s\n]+or[\s\n]+monitoring[\s\n]+of[\s\n]+the[\s\n]+content[\s\n]+of[\s\n]+privileged[\s\n]+communications,[\s\n]+or[\s\n]+work[\s\n]+product,[\s\n]+related[\s\n]+to[\s\n]+personal[\s\n]+representation[\s\n]+or[\s\n]+services[\s\n]+by[\s\n]+attorneys,[\s\n]+psychotherapists,[\s\n]+or[\s\n]+clergy,[\s\n]+and[\s\n]+their[\s\n]+assistants\.[\s\n]+Such[\s\n]+communications[\s\n]+and[\s\n]+work[\s\n]+product[\s\n]+are[\s\n]+private[\s\n]+and[\s\n]+confidential\.[\s\n]+See[\s\n]+User[\s\n]+Agreement[\s\n]+for[\s\n]+details\.|I've[\s\n]+read[\s\n]+\&[\s\n]+consent[\s\n]+to[\s\n]+terms[\s\n]+in[\s\n]+IS[\s\n]+user[\s\n]+agreem't\.)$ ^You[\s\n]+are[\s\n]+accessing[\s\n]+a[\s\n]+U\.S\.[\s\n]+Government[\s\n]+\(USG\)[\s\n]+Information[\s\n]+System[\s\n]+\(IS\)[\s\n]+that[\s\n]+is[\s\n]+provided[\s\n]+for[\s\n]+USG\-authorized[\s\n]+use[\s\n]+only\.[\s\n]+By[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+\(which[\s\n]+includes[\s\n]+any[\s\n]+device[\s\n]+attached[\s\n]+to[\s\n]+this[\s\n]+IS\),[\s\n]+you[\s\n]+consent[\s\n]+to[\s\n]+the[\s\n]+following[\s\n]+conditions\:(?:[\n]+|(?:\\n)+)\-The[\s\n]+USG[\s\n]+routinely[\s\n]+intercepts[\s\n]+and[\s\n]+monitors[\s\n]+communications[\s\n]+on[\s\n]+this[\s\n]+IS[\s\n]+for[\s\n]+purposes[\s\n]+including,[\s\n]+but[\s\n]+not[\s\n]+limited[\s\n]+to,[\s\n]+penetration[\s\n]+testing,[\s\n]+COMSEC[\s\n]+monitoring,[\s\n]+network[\s\n]+operations[\s\n]+and[\s\n]+defense,[\s\n]+personnel[\s\n]+misconduct[\s\n]+\(PM\),[\s\n]+law[\s\n]+enforcement[\s\n]+\(LE\),[\s\n]+and[\s\n]+counterintelligence[\s\n]+\(CI\)[\s\n]+investigations\.(?:[\n]+|(?:\\n)+)\-At[\s\n]+any[\s\n]+time,[\s\n]+the[\s\n]+USG[\s\n]+may[\s\n]+inspect[\s\n]+and[\s\n]+seize[\s\n]+data[\s\n]+stored[\s\n]+on[\s\n]+this[\s\n]+IS\.(?:[\n]+|(?:\\n)+)\-Communications[\s\n]+using,[\s\n]+or[\s\n]+data[\s\n]+stored[\s\n]+on,[\s\n]+this[\s\n]+IS[\s\n]+are[\s\n]+not[\s\n]+private,[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+routine[\s\n]+monitoring,[\s\n]+interception,[\s\n]+and[\s\n]+search,[\s\n]+and[\s\n]+may[\s\n]+be[\s\n]+disclosed[\s\n]+or[\s\n]+used[\s\n]+for[\s\n]+any[\s\n]+USG\-authorized[\s\n]+purpose\.(?:[\n]+|(?:\\n)+)\-This[\s\n]+IS[\s\n]+includes[\s\n]+security[\s\n]+measures[\s\n]+\(e\.g\.,[\s\n]+authentication[\s\n]+and[\s\n]+access[\s\n]+controls\)[\s\n]+to[\s\n]+protect[\s\n]+USG[\s\n]+interests\-\-not[\s\n]+for[\s\n]+your[\s\n]+personal[\s\n]+benefit[\s\n]+or[\s\n]+privacy\.(?:[\n]+|(?:\\n)+)\-Notwithstanding[\s\n]+the[\s\n]+above,[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+does[\s\n]+not[\s\n]+constitute[\s\n]+consent[\s\n]+to[\s\n]+PM,[\s\n]+LE[\s\n]+or[\s\n]+CI[\s\n]+investigative[\s\n]+searching[\s\n]+or[\s\n]+monitoring[\s\n]+of[\s\n]+the[\s\n]+content[\s\n]+of[\s\n]+privileged[\s\n]+communications,[\s\n]+or[\s\n]+work[\s\n]+product,[\s\n]+related[\s\n]+to[\s\n]+personal[\s\n]+representation[\s\n]+or[\s\n]+services[\s\n]+by[\s\n]+attorneys,[\s\n]+psychotherapists,[\s\n]+or[\s\n]+clergy,[\s\n]+and[\s\n]+their[\s\n]+assistants\.[\s\n]+Such[\s\n]+communications[\s\n]+and[\s\n]+work[\s\n]+product[\s\n]+are[\s\n]+private[\s\n]+and[\s\n]+confidential\.[\s\n]+See[\s\n]+User[\s\n]+Agreement[\s\n]+for[\s\n]+details\.$ ^I've[\s\n]+read[\s\n]+\&[\s\n]+consent[\s\n]+to[\s\n]+terms[\s\n]+in[\s\n]+IS[\s\n]+user[\s\n]+agreem't\.$ ^Use[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+constitutes[\s\n]+consent[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times\.[\s\n]+This[\s\n]+is[\s\n]+a[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system\.[\s\n]+All[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+and[\s\n]+related[\s\n]+equipment[\s\n]+are[\s\n]+intended[\s\n]+for[\s\n]+the[\s\n]+communication,[\s\n]+transmission,[\s\n]+processing,[\s\n]+and[\s\n]+storage[\s\n]+of[\s\n]+official[\s\n]+U\.S\.[\s\n]+Government[\s\n]+or[\s\n]+other[\s\n]+authorized[\s\n]+information[\s\n]+only\.[\s\n]+All[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times[\s\n]+to[\s\n]+ensure[\s\n]+proper[\s\n]+functioning[\s\n]+of[\s\n]+equipment[\s\n]+and[\s\n]+systems[\s\n]+including[\s\n]+security[\s\n]+devices[\s\n]+and[\s\n]+systems,[\s\n]+to[\s\n]+prevent[\s\n]+unauthorized[\s\n]+use[\s\n]+and[\s\n]+violations[\s\n]+of[\s\n]+statutes[\s\n]+and[\s\n]+security[\s\n]+regulations,[\s\n]+to[\s\n]+deter[\s\n]+criminal[\s\n]+activity,[\s\n]+and[\s\n]+for[\s\n]+other[\s\n]+similar[\s\n]+purposes\.[\s\n]+Any[\s\n]+user[\s\n]+of[\s\n]+a[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+should[\s\n]+be[\s\n]+aware[\s\n]+that[\s\n]+any[\s\n]+information[\s\n]+placed[\s\n]+in[\s\n]+the[\s\n]+system[\s\n]+is[\s\n]+subject[\s\n]+to[\s\n]+monitoring[\s\n]+and[\s\n]+is[\s\n]+not[\s\n]+subject[\s\n]+to[\s\n]+any[\s\n]+expectation[\s\n]+of[\s\n]+privacy\.[\s\n]+If[\s\n]+monitoring[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+reveals[\s\n]+possible[\s\n]+evidence[\s\n]+of[\s\n]+violation[\s\n]+of[\s\n]+criminal[\s\n]+statutes,[\s\n]+this[\s\n]+evidence[\s\n]+and[\s\n]+any[\s\n]+other[\s\n]+related[\s\n]+information,[\s\n]+including[\s\n]+identification[\s\n]+information[\s\n]+about[\s\n]+the[\s\n]+user,[\s\n]+may[\s\n]+be[\s\n]+provided[\s\n]+to[\s\n]+law[\s\n]+enforcement[\s\n]+officials\.[\s\n]+If[\s\n]+monitoring[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+reveals[\s\n]+violations[\s\n]+of[\s\n]+security[\s\n]+regulations[\s\n]+or[\s\n]+unauthorized[\s\n]+use,[\s\n]+employees[\s\n]+who[\s\n]+violate[\s\n]+security[\s\n]+regulations[\s\n]+or[\s\n]+make[\s\n]+unauthorized[\s\n]+use[\s\n]+of[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+appropriate[\s\n]+disciplinary[\s\n]+action\.[\s\n]+Use[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+constitutes[\s\n]+consent[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times\.$ ^\-\-[\s\n]+WARNING[\s\n]+\-\-[\s\n]+This[\s\n]+system[\s\n]+is[\s\n]+for[\s\n]+the[\s\n]+use[\s\n]+of[\s\n]+authorized[\s\n]+users[\s\n]+only\.[\s\n]+Individuals[\s\n]+using[\s\n]+this[\s\n]+computer[\s\n]+system[\s\n]+without[\s\n]+authority[\s\n]+or[\s\n]+in[\s\n]+excess[\s\n]+of[\s\n]+their[\s\n]+authority[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+having[\s\n]+all[\s\n]+their[\s\n]+activities[\s\n]+on[\s\n]+this[\s\n]+system[\s\n]+monitored[\s\n]+and[\s\n]+recorded[\s\n]+by[\s\n]+system[\s\n]+personnel\.[\s\n]+Anyone[\s\n]+using[\s\n]+this[\s\n]+system[\s\n]+expressly[\s\n]+consents[\s\n]+to[\s\n]+such[\s\n]+monitoring[\s\n]+and[\s\n]+is[\s\n]+advised[\s\n]+that[\s\n]+if[\s\n]+such[\s\n]+monitoring[\s\n]+reveals[\s\n]+possible[\s\n]+evidence[\s\n]+of[\s\n]+criminal[\s\n]+activity[\s\n]+system[\s\n]+personal[\s\n]+may[\s\n]+provide[\s\n]+the[\s\n]+evidence[\s\n]+of[\s\n]+such[\s\n]+monitoring[\s\n]+to[\s\n]+law[\s\n]+enforcement[\s\n]+officials\.$ ^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ Remote Login Banner Verbiage Enter an appropriate login banner for your organization. Please note that new lines must be expressed by the '\n' character and special characters like parentheses and quotation marks must be escaped with '\\'. ^(Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$ ^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ ^(You[\s\n]+are[\s\n]+accessing[\s\n]+a[\s\n]+U\.S\.[\s\n]+Government[\s\n]+\(USG\)[\s\n]+Information[\s\n]+System[\s\n]+\(IS\)[\s\n]+that[\s\n]+is[\s\n]+provided[\s\n]+for[\s\n]+USG\-authorized[\s\n]+use[\s\n]+only\.[\s\n]+By[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+\(which[\s\n]+includes[\s\n]+any[\s\n]+device[\s\n]+attached[\s\n]+to[\s\n]+this[\s\n]+IS\),[\s\n]+you[\s\n]+consent[\s\n]+to[\s\n]+the[\s\n]+following[\s\n]+conditions\:(?:[\n]+|(?:\\n)+)\-The[\s\n]+USG[\s\n]+routinely[\s\n]+intercepts[\s\n]+and[\s\n]+monitors[\s\n]+communications[\s\n]+on[\s\n]+this[\s\n]+IS[\s\n]+for[\s\n]+purposes[\s\n]+including,[\s\n]+but[\s\n]+not[\s\n]+limited[\s\n]+to,[\s\n]+penetration[\s\n]+testing,[\s\n]+COMSEC[\s\n]+monitoring,[\s\n]+network[\s\n]+operations[\s\n]+and[\s\n]+defense,[\s\n]+personnel[\s\n]+misconduct[\s\n]+\(PM\),[\s\n]+law[\s\n]+enforcement[\s\n]+\(LE\),[\s\n]+and[\s\n]+counterintelligence[\s\n]+\(CI\)[\s\n]+investigations\.(?:[\n]+|(?:\\n)+)\-At[\s\n]+any[\s\n]+time,[\s\n]+the[\s\n]+USG[\s\n]+may[\s\n]+inspect[\s\n]+and[\s\n]+seize[\s\n]+data[\s\n]+stored[\s\n]+on[\s\n]+this[\s\n]+IS\.(?:[\n]+|(?:\\n)+)\-Communications[\s\n]+using,[\s\n]+or[\s\n]+data[\s\n]+stored[\s\n]+on,[\s\n]+this[\s\n]+IS[\s\n]+are[\s\n]+not[\s\n]+private,[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+routine[\s\n]+monitoring,[\s\n]+interception,[\s\n]+and[\s\n]+search,[\s\n]+and[\s\n]+may[\s\n]+be[\s\n]+disclosed[\s\n]+or[\s\n]+used[\s\n]+for[\s\n]+any[\s\n]+USG\-authorized[\s\n]+purpose\.(?:[\n]+|(?:\\n)+)\-This[\s\n]+IS[\s\n]+includes[\s\n]+security[\s\n]+measures[\s\n]+\(e\.g\.,[\s\n]+authentication[\s\n]+and[\s\n]+access[\s\n]+controls\)[\s\n]+to[\s\n]+protect[\s\n]+USG[\s\n]+interests\-\-not[\s\n]+for[\s\n]+your[\s\n]+personal[\s\n]+benefit[\s\n]+or[\s\n]+privacy\.(?:[\n]+|(?:\\n)+)\-Notwithstanding[\s\n]+the[\s\n]+above,[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+does[\s\n]+not[\s\n]+constitute[\s\n]+consent[\s\n]+to[\s\n]+PM,[\s\n]+LE[\s\n]+or[\s\n]+CI[\s\n]+investigative[\s\n]+searching[\s\n]+or[\s\n]+monitoring[\s\n]+of[\s\n]+the[\s\n]+content[\s\n]+of[\s\n]+privileged[\s\n]+communications,[\s\n]+or[\s\n]+work[\s\n]+product,[\s\n]+related[\s\n]+to[\s\n]+personal[\s\n]+representation[\s\n]+or[\s\n]+services[\s\n]+by[\s\n]+attorneys,[\s\n]+psychotherapists,[\s\n]+or[\s\n]+clergy,[\s\n]+and[\s\n]+their[\s\n]+assistants\.[\s\n]+Such[\s\n]+communications[\s\n]+and[\s\n]+work[\s\n]+product[\s\n]+are[\s\n]+private[\s\n]+and[\s\n]+confidential\.[\s\n]+See[\s\n]+User[\s\n]+Agreement[\s\n]+for[\s\n]+details\.|I've[\s\n]+read[\s\n]+\&[\s\n]+consent[\s\n]+to[\s\n]+terms[\s\n]+in[\s\n]+IS[\s\n]+user[\s\n]+agreem't\.)$ ^You[\s\n]+are[\s\n]+accessing[\s\n]+a[\s\n]+U\.S\.[\s\n]+Government[\s\n]+\(USG\)[\s\n]+Information[\s\n]+System[\s\n]+\(IS\)[\s\n]+that[\s\n]+is[\s\n]+provided[\s\n]+for[\s\n]+USG\-authorized[\s\n]+use[\s\n]+only\.[\s\n]+By[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+\(which[\s\n]+includes[\s\n]+any[\s\n]+device[\s\n]+attached[\s\n]+to[\s\n]+this[\s\n]+IS\),[\s\n]+you[\s\n]+consent[\s\n]+to[\s\n]+the[\s\n]+following[\s\n]+conditions\:(?:[\n]+|(?:\\n)+)\-The[\s\n]+USG[\s\n]+routinely[\s\n]+intercepts[\s\n]+and[\s\n]+monitors[\s\n]+communications[\s\n]+on[\s\n]+this[\s\n]+IS[\s\n]+for[\s\n]+purposes[\s\n]+including,[\s\n]+but[\s\n]+not[\s\n]+limited[\s\n]+to,[\s\n]+penetration[\s\n]+testing,[\s\n]+COMSEC[\s\n]+monitoring,[\s\n]+network[\s\n]+operations[\s\n]+and[\s\n]+defense,[\s\n]+personnel[\s\n]+misconduct[\s\n]+\(PM\),[\s\n]+law[\s\n]+enforcement[\s\n]+\(LE\),[\s\n]+and[\s\n]+counterintelligence[\s\n]+\(CI\)[\s\n]+investigations\.(?:[\n]+|(?:\\n)+)\-At[\s\n]+any[\s\n]+time,[\s\n]+the[\s\n]+USG[\s\n]+may[\s\n]+inspect[\s\n]+and[\s\n]+seize[\s\n]+data[\s\n]+stored[\s\n]+on[\s\n]+this[\s\n]+IS\.(?:[\n]+|(?:\\n)+)\-Communications[\s\n]+using,[\s\n]+or[\s\n]+data[\s\n]+stored[\s\n]+on,[\s\n]+this[\s\n]+IS[\s\n]+are[\s\n]+not[\s\n]+private,[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+routine[\s\n]+monitoring,[\s\n]+interception,[\s\n]+and[\s\n]+search,[\s\n]+and[\s\n]+may[\s\n]+be[\s\n]+disclosed[\s\n]+or[\s\n]+used[\s\n]+for[\s\n]+any[\s\n]+USG\-authorized[\s\n]+purpose\.(?:[\n]+|(?:\\n)+)\-This[\s\n]+IS[\s\n]+includes[\s\n]+security[\s\n]+measures[\s\n]+\(e\.g\.,[\s\n]+authentication[\s\n]+and[\s\n]+access[\s\n]+controls\)[\s\n]+to[\s\n]+protect[\s\n]+USG[\s\n]+interests\-\-not[\s\n]+for[\s\n]+your[\s\n]+personal[\s\n]+benefit[\s\n]+or[\s\n]+privacy\.(?:[\n]+|(?:\\n)+)\-Notwithstanding[\s\n]+the[\s\n]+above,[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+does[\s\n]+not[\s\n]+constitute[\s\n]+consent[\s\n]+to[\s\n]+PM,[\s\n]+LE[\s\n]+or[\s\n]+CI[\s\n]+investigative[\s\n]+searching[\s\n]+or[\s\n]+monitoring[\s\n]+of[\s\n]+the[\s\n]+content[\s\n]+of[\s\n]+privileged[\s\n]+communications,[\s\n]+or[\s\n]+work[\s\n]+product,[\s\n]+related[\s\n]+to[\s\n]+personal[\s\n]+representation[\s\n]+or[\s\n]+services[\s\n]+by[\s\n]+attorneys,[\s\n]+psychotherapists,[\s\n]+or[\s\n]+clergy,[\s\n]+and[\s\n]+their[\s\n]+assistants\.[\s\n]+Such[\s\n]+communications[\s\n]+and[\s\n]+work[\s\n]+product[\s\n]+are[\s\n]+private[\s\n]+and[\s\n]+confidential\.[\s\n]+See[\s\n]+User[\s\n]+Agreement[\s\n]+for[\s\n]+details\.$ ^I've[\s\n]+read[\s\n]+\&[\s\n]+consent[\s\n]+to[\s\n]+terms[\s\n]+in[\s\n]+IS[\s\n]+user[\s\n]+agreem't\.$ ^Use[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+constitutes[\s\n]+consent[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times\.[\s\n]+This[\s\n]+is[\s\n]+a[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system\.[\s\n]+All[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+and[\s\n]+related[\s\n]+equipment[\s\n]+are[\s\n]+intended[\s\n]+for[\s\n]+the[\s\n]+communication,[\s\n]+transmission,[\s\n]+processing,[\s\n]+and[\s\n]+storage[\s\n]+of[\s\n]+official[\s\n]+U\.S\.[\s\n]+Government[\s\n]+or[\s\n]+other[\s\n]+authorized[\s\n]+information[\s\n]+only\.[\s\n]+All[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times[\s\n]+to[\s\n]+ensure[\s\n]+proper[\s\n]+functioning[\s\n]+of[\s\n]+equipment[\s\n]+and[\s\n]+systems[\s\n]+including[\s\n]+security[\s\n]+devices[\s\n]+and[\s\n]+systems,[\s\n]+to[\s\n]+prevent[\s\n]+unauthorized[\s\n]+use[\s\n]+and[\s\n]+violations[\s\n]+of[\s\n]+statutes[\s\n]+and[\s\n]+security[\s\n]+regulations,[\s\n]+to[\s\n]+deter[\s\n]+criminal[\s\n]+activity,[\s\n]+and[\s\n]+for[\s\n]+other[\s\n]+similar[\s\n]+purposes\.[\s\n]+Any[\s\n]+user[\s\n]+of[\s\n]+a[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+should[\s\n]+be[\s\n]+aware[\s\n]+that[\s\n]+any[\s\n]+information[\s\n]+placed[\s\n]+in[\s\n]+the[\s\n]+system[\s\n]+is[\s\n]+subject[\s\n]+to[\s\n]+monitoring[\s\n]+and[\s\n]+is[\s\n]+not[\s\n]+subject[\s\n]+to[\s\n]+any[\s\n]+expectation[\s\n]+of[\s\n]+privacy\.[\s\n]+If[\s\n]+monitoring[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+reveals[\s\n]+possible[\s\n]+evidence[\s\n]+of[\s\n]+violation[\s\n]+of[\s\n]+criminal[\s\n]+statutes,[\s\n]+this[\s\n]+evidence[\s\n]+and[\s\n]+any[\s\n]+other[\s\n]+related[\s\n]+information,[\s\n]+including[\s\n]+identification[\s\n]+information[\s\n]+about[\s\n]+the[\s\n]+user,[\s\n]+may[\s\n]+be[\s\n]+provided[\s\n]+to[\s\n]+law[\s\n]+enforcement[\s\n]+officials\.[\s\n]+If[\s\n]+monitoring[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+reveals[\s\n]+violations[\s\n]+of[\s\n]+security[\s\n]+regulations[\s\n]+or[\s\n]+unauthorized[\s\n]+use,[\s\n]+employees[\s\n]+who[\s\n]+violate[\s\n]+security[\s\n]+regulations[\s\n]+or[\s\n]+make[\s\n]+unauthorized[\s\n]+use[\s\n]+of[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+systems[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+appropriate[\s\n]+disciplinary[\s\n]+action\.[\s\n]+Use[\s\n]+of[\s\n]+this[\s\n]+or[\s\n]+any[\s\n]+other[\s\n]+DoD[\s\n]+interest[\s\n]+computer[\s\n]+system[\s\n]+constitutes[\s\n]+consent[\s\n]+to[\s\n]+monitoring[\s\n]+at[\s\n]+all[\s\n]+times\.$ ^\-\-[\s\n]+WARNING[\s\n]+\-\-[\s\n]+This[\s\n]+system[\s\n]+is[\s\n]+for[\s\n]+the[\s\n]+use[\s\n]+of[\s\n]+authorized[\s\n]+users[\s\n]+only\.[\s\n]+Individuals[\s\n]+using[\s\n]+this[\s\n]+computer[\s\n]+system[\s\n]+without[\s\n]+authority[\s\n]+or[\s\n]+in[\s\n]+excess[\s\n]+of[\s\n]+their[\s\n]+authority[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+having[\s\n]+all[\s\n]+their[\s\n]+activities[\s\n]+on[\s\n]+this[\s\n]+system[\s\n]+monitored[\s\n]+and[\s\n]+recorded[\s\n]+by[\s\n]+system[\s\n]+personnel\.[\s\n]+Anyone[\s\n]+using[\s\n]+this[\s\n]+system[\s\n]+expressly[\s\n]+consents[\s\n]+to[\s\n]+such[\s\n]+monitoring[\s\n]+and[\s\n]+is[\s\n]+advised[\s\n]+that[\s\n]+if[\s\n]+such[\s\n]+monitoring[\s\n]+reveals[\s\n]+possible[\s\n]+evidence[\s\n]+of[\s\n]+criminal[\s\n]+activity[\s\n]+system[\s\n]+personal[\s\n]+may[\s\n]+provide[\s\n]+the[\s\n]+evidence[\s\n]+of[\s\n]+such[\s\n]+monitoring[\s\n]+to[\s\n]+law[\s\n]+enforcement[\s\n]+officials\.$ ^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ Modify the System Login Banner To configure the system login banner edit /etc/issue. Replace the default text with a message compliant with the local site policy or a legal disclaimer. The DoD required text is either: You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions: -The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations. -At any time, the USG may inspect and seize data stored on this IS. -Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose. -This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy. -Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details. OR: I've read & consent to terms in IS user agreem't. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.9 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-8(a) AC-8(c) PR.AC-7 SRG-OS-000023-GPOS-00006 SRG-OS-000228-GPOS-00088 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist. Ensure Local Login Warning Banner Is Configured Properly To configure the system local login warning banner edit the /etc/issue file. The contents of this file is displayed to users prior to login to local terminals. Replace the default text with a message compliant with the local site policy. The message should not contain information about operating system version, release, kernel version or patch level. The recommended banner text can be tailored in the XCCDF Value xccdf_org.ssgproject.content_value_cis_banner_text: 1.7.2 Warning messages inform users who are attempting to login to the system of their legal status regarding the system and must include the name of the organization that owns the system and any monitoring policies that are in place. Displaying OS and patch level information in login banners also has the side effect of providing detailed system information to attackers attempting to target specific exploits of a system. Authorized users can easily get this information by running the uname -a command once they have logged in. Modify the System Login Banner for Remote Connections To configure the system login banner edit /etc/issue.net. Replace the default text with a message compliant with the local site policy or a legal disclaimer. The DoD required text is either: You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions: -The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations. -At any time, the USG may inspect and seize data stored on this IS. -Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose. -This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy. -Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details. OR: I've read & consent to terms in IS user agreem't. SRG-OS-000023-GPOS-00006 SRG-OS-000228-GPOS-00088 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist. Ensure Remote Login Warning Banner Is Configured Properly To configure the system remote login warning banner edit the /etc/issue.net file. The contents of this file is displayed to users prior to login from remote connections. Replace the default text with a message compliant with the local site policy. The message should not contain information about operating system version, release, kernel version or patch level. The recommended banner text can be tailored in the XCCDF Value xccdf_org.ssgproject.content_value_cis_banner_text: 1.7.3 Warning messages inform users who are attempting to login to the system of their legal status regarding the system and must include the name of the organization that owns the system and any monitoring policies that are in place. Displaying OS and patch level information in login banners also has the side effect of providing detailed system information to attackers attempting to target specific exploits of a system. Authorized users can easily get this information by running the uname -a command once they have logged in. Modify the System Message of the Day Banner To configure the system message banner edit /etc/motd. Replace the default text with a message compliant with the local site policy or a legal disclaimer. The DoD required text is either: You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions: -The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations. -At any time, the USG may inspect and seize data stored on this IS. -Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose. -This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy. -Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details. OR: I've read & consent to terms in IS user agreem't. Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist. Ensure Message Of The Day Is Configured Properly To configure the system message of the day banner edit the /etc/motd file. Replace the default text with a message compliant with the local site policy. The message should not contain information about operating system version, release, kernel version or patch level. The recommended banner text can be tailored in the XCCDF Value xccdf_org.ssgproject.content_value_cis_banner_text: 1.7.1 Warning messages inform users who are attempting to login to the system of their legal status regarding the system and must include the name of the organization that owns the system and any monitoring policies that are in place. Displaying OS and patch level information in login banners also has the side effect of providing detailed system information to attackers attempting to target specific exploits of a system. Authorized users can easily get this information by running the uname -a command once they have logged in. Verify Group Ownership of System Login Banner To properly set the group owner of /etc/issue, run the command: $ sudo chgrp root /etc/issue 1.7.5 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper group ownership will ensure that only root user can modify the banner. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/issue" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/issue fi fi - name: Set the file_groupowner_etc_issue_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_issue_newgroup: '0' tags: - configure_strategy - file_groupowner_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/issue ansible.builtin.stat: path: /etc/issue register: file_exists tags: - configure_strategy - file_groupowner_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/issue ansible.builtin.file: path: /etc/issue follow: false group: '{{ file_groupowner_etc_issue_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupowner_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Ownership of System Login Banner for Remote Connections To properly set the group owner of /etc/issue.net, run the command: $ sudo chgrp root /etc/issue.net 1.7.6 1.2.8 1.2 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper group ownership will ensure that only root user can modify the banner. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/issue.net" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/issue.net fi fi - name: Set the file_groupowner_etc_issue_net_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_issue_net_newgroup: '0' tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_groupowner_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/issue.net ansible.builtin.stat: path: /etc/issue.net register: file_exists tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_groupowner_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/issue.net ansible.builtin.file: path: /etc/issue.net follow: false group: '{{ file_groupowner_etc_issue_net_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_groupowner_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Ownership of Message of the Day Banner To properly set the group owner of /etc/motd, run the command: $ sudo chgrp root /etc/motd 1.7.4 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper group ownership will ensure that only root user can modify the banner. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/motd" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/motd fi fi - name: Set the file_groupowner_etc_motd_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_motd_newgroup: '0' tags: - configure_strategy - file_groupowner_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/motd ansible.builtin.stat: path: /etc/motd register: file_exists tags: - configure_strategy - file_groupowner_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/motd ansible.builtin.file: path: /etc/motd follow: false group: '{{ file_groupowner_etc_motd_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupowner_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify ownership of System Login Banner To properly set the owner of /etc/issue, run the command: $ sudo chown root /etc/issue 1.7.5 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper ownership will ensure that only root user can modify the banner. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/issue" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/issue fi fi - name: Set the file_owner_etc_issue_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_issue_newown: '0' tags: - configure_strategy - file_owner_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/issue ansible.builtin.stat: path: /etc/issue register: file_exists tags: - configure_strategy - file_owner_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/issue ansible.builtin.file: path: /etc/issue follow: false owner: '{{ file_owner_etc_issue_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_owner_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify ownership of System Login Banner for Remote Connections To properly set the owner of /etc/issue.net, run the command: $ sudo chown root /etc/issue.net 1.7.6 1.2.8 1.2 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper ownership will ensure that only root user can modify the banner. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/issue.net" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/issue.net fi fi - name: Set the file_owner_etc_issue_net_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_issue_net_newown: '0' tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_owner_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/issue.net ansible.builtin.stat: path: /etc/issue.net register: file_exists tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_owner_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/issue.net ansible.builtin.file: path: /etc/issue.net follow: false owner: '{{ file_owner_etc_issue_net_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_owner_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify ownership of Message of the Day Banner To properly set the owner of /etc/motd, run the command: $ sudo chown root /etc/motd 1.7.4 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper ownership will ensure that only root user can modify the banner. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/motd" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/motd fi fi - name: Set the file_owner_etc_motd_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_motd_newown: '0' tags: - configure_strategy - file_owner_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/motd ansible.builtin.stat: path: /etc/motd register: file_exists tags: - configure_strategy - file_owner_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/motd ansible.builtin.file: path: /etc/motd follow: false owner: '{{ file_owner_etc_motd_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_owner_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify permissions on System Login Banner To properly set the permissions of /etc/issue, run the command: $ sudo chmod 0644 /etc/issue 1.7.5 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper permissions will ensure that only root user can modify the banner. chmod u-xs,g-xws,o-xwt /etc/issue - name: Test for existence /etc/issue ansible.builtin.stat: path: /etc/issue register: file_exists tags: - configure_strategy - file_permissions_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/issue ansible.builtin.file: path: /etc/issue mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_etc_issue - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify permissions on System Login Banner for Remote Connections To properly set the permissions of /etc/issue.net, run the command: $ sudo chmod 0644 /etc/issue.net 1.7.6 1.2.8 1.2 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper permissions will ensure that only root user can modify the banner. chmod u-xs,g-xws,o-xwt /etc/issue.net - name: Test for existence /etc/issue.net ansible.builtin.stat: path: /etc/issue.net register: file_exists tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_permissions_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/issue.net ansible.builtin.file: path: /etc/issue.net mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - configure_strategy - file_permissions_etc_issue_net - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify permissions on Message of the Day Banner To properly set the permissions of /etc/motd, run the command: $ sudo chmod 0644 /etc/motd 1.7.4 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. Proper permissions will ensure that only root user can modify the banner. chmod u-xs,g-xws,o-xwt /etc/motd - name: Test for existence /etc/motd ansible.builtin.stat: path: /etc/motd register: file_exists tags: - configure_strategy - file_permissions_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/motd ansible.builtin.file: path: /etc/motd mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_etc_motd - low_complexity - low_disruption - medium_severity - no_reboot_needed Implement a GUI Warning Banner In the default graphical environment, users logging directly into the system are greeted with a login screen provided by the GNOME Display Manager (GDM). The warning banner should be displayed in this graphical environment for these users. The following sections describe how to configure the GDM login banner. Enable GNOME3 Login Warning Banner In the default graphical environment, displaying a login warning banner in the GNOME Display Manager's login screen can be enabled on the login screen by setting banner-message-enable to true. To enable, add or edit banner-message-enable to /etc/dconf/db/distro.d/00-security-settings. For example: [org/gnome/login-screen] banner-message-enable=true Once the setting has been added, add a lock to /etc/dconf/db/distro.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/login-screen/banner-message-enable After the settings have been set, run dconf update. The banner text must also be set. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.9 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-8(a) AC-8(b) AC-8(c) PR.AC-7 SRG-OS-000023-GPOS-00006 SRG-OS-000228-GPOS-00088 1.8.1 Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. For U.S. Government systems, system use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/distro.d/00-security-settings" DBDIR="/etc/dconf/db/distro.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*banner-message-enable\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)banner-message-enable(\s*=)/#\1banner-message-enable\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")" if grep -q "^\\s*banner-message-enable\\s*=" "${DCONFFILE}" then sed -i "s/\\s*banner-message-enable\\s*=\\s*.*/banner-message-enable=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/login-screen\\]|a\\banner-message-enable=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/login-screen/banner-message-enable$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/distro.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/login-screen/banner-message-enable$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/login-screen/banner-message-enable$" /etc/dconf/db/distro.d/ then echo "/org/gnome/login-screen/banner-message-enable" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(b) - NIST-800-53-AC-8(c) - dconf_gnome_banner_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Enable GNOME3 Login Warning Banner community.general.ini_file: dest: /etc/dconf/db/distro.d/00-security-settings section: org/gnome/login-screen option: banner-message-enable value: 'true' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(b) - NIST-800-53-AC-8(c) - dconf_gnome_banner_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of GNOME banner-message-enabled ansible.builtin.lineinfile: path: /etc/dconf/db/distro.d/locks/00-security-settings-lock regexp: ^/org/gnome/login-screen/banner-message-enable$ line: /org/gnome/login-screen/banner-message-enable create: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(b) - NIST-800-53-AC-8(c) - dconf_gnome_banner_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(b) - NIST-800-53-AC-8(c) - dconf_gnome_banner_enabled - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Set the GNOME3 Login Warning Banner Text In the default graphical environment, configuring the login warning banner text in the GNOME Display Manager's login screen can be configured on the login screen by setting banner-message-text to 'APPROVED_BANNER' where APPROVED_BANNER is the approved banner for your environment. To enable, add or edit banner-message-text to /etc/dconf/db/distro.d/00-security-settings. For example: [org/gnome/login-screen] banner-message-text='APPROVED_BANNER' Once the setting has been added, add a lock to /etc/dconf/db/distro.d/locks/00-security-settings-lock to prevent user modification. For example: /org/gnome/login-screen/banner-message-text After the settings have been set, run dconf update. When entering a warning banner that spans several lines, remember to begin and end the string with ' and use \n for new lines. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.9 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-8(a) AC-8(c) PR.AC-7 SRG-OS-000023-GPOS-00006 SRG-OS-000228-GPOS-00088 1.8.1 An appropriate warning message reinforces policy awareness during the logon process and facilitates possible legal action against attackers. # Remediation is applicable only in certain platforms if rpm --quiet -q gdm; then login_banner_text="" # Multiple regexes transform the banner regex into a usable banner # 0 - Remove anchors around the banner text login_banner_text=$(echo "$login_banner_text" | sed 's/^\^\(.*\)\$$/\1/g') # 1 - Keep only the first banners if there are multiple # (dod_banners contains the long and short banner) login_banner_text=$(echo "$login_banner_text" | sed 's/^(\(.*\.\)|.*)$/\1/g') # 2 - Add spaces ' '. (Transforms regex for "space or newline" into a " ") login_banner_text=$(echo "$login_banner_text" | sed 's/\[\\s\\n\]+/ /g') # 3 - Adds newline "tokens". (Transforms "(?:\[\\n\]+|(?:\\n)+)" into "(n)*") login_banner_text=$(echo "$login_banner_text" | sed 's/(?:\[\\n\]+|(?:\\n)+)/(n)*/g') # 4 - Remove any leftover backslash. (From any parethesis in the banner, for example). login_banner_text=$(echo "$login_banner_text" | sed 's/\\//g') # 5 - Removes the newline "token." (Transforms them into newline escape sequences "\n"). # ( Needs to be done after 4, otherwise the escapce sequence will become just "n". login_banner_text=$(echo "$login_banner_text" | sed 's/(n)\*/\\n/g') # Check for setting in any of the DConf db directories # If files contain ibus or distro, ignore them. # The assignment assumes that individual filenames don't contain : readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1) DCONFFILE="/etc/dconf/db/distro.d/00-security-settings" DBDIR="/etc/dconf/db/distro.d" mkdir -p "${DBDIR}" # Comment out the configurations in databases different from the target one if [ "${#SETTINGSFILES[@]}" -ne 0 ] then if grep -q "^\\s*banner-message-text\\s*=" "${SETTINGSFILES[@]}" then sed -Ei "s/(^\s*)banner-message-text(\s*=)/#\1banner-message-text\2/g" "${SETTINGSFILES[@]}" fi fi [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}" if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}" then printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE} fi escaped_value="$(sed -e 's/\\/\\\\/g' <<< "'${login_banner_text}'")" if grep -q "^\\s*banner-message-text\\s*=" "${DCONFFILE}" then sed -i "s/\\s*banner-message-text\\s*=\\s*.*/banner-message-text=${escaped_value}/g" "${DCONFFILE}" else sed -i "\\|\\[org/gnome/login-screen\\]|a\\banner-message-text=${escaped_value}" "${DCONFFILE}" fi dconf update # Check for setting in any of the DConf db directories LOCKFILES=$(grep -r "^/org/gnome/login-screen/banner-message-text$" "/etc/dconf/db/" \ | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1) LOCKSFOLDER="/etc/dconf/db/distro.d/locks" mkdir -p "${LOCKSFOLDER}" # Comment out the configurations in databases different from the target one if [[ ! -z "${LOCKFILES}" ]] then sed -i -E "s|^/org/gnome/login-screen/banner-message-text$|#&|" "${LOCKFILES[@]}" fi if ! grep -qr "^/org/gnome/login-screen/banner-message-text$" /etc/dconf/db/distro.d/ then echo "/org/gnome/login-screen/banner-message-text" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock" fi dconf update else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - dconf_gnome_login_banner_text - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: XCCDF Value login_banner_text # promote to variable set_fact: login_banner_text: !!str tags: - always - name: Set the GNOME3 Login Warning Banner Text ansible.builtin.file: path: /etc/dconf/db/{{ item }} owner: root group: root mode: 493 state: directory with_items: - distro.d - distro.d/locks when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - dconf_gnome_login_banner_text - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Set the GNOME3 Login Warning Banner Text ansible.builtin.file: path: /etc/dconf/db/distro.d/{{ item }} owner: root group: root mode: 420 state: touch with_items: - 00-security-settings - locks/00-security-settings-lock when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - dconf_gnome_login_banner_text - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Set the GNOME3 Login Warning Banner Text community.general.ini_file: dest: /etc/dconf/db/distro.d/00-security-settings section: org/gnome/login-screen option: banner-message-text value: '''{{ login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*\.)\|.*\)$", "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)", "(n)*") | regex_replace("\\", "") | regex_replace("\(n\)\*", "\\n") }}''' create: true no_extra_spaces: true when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - dconf_gnome_login_banner_text - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Prevent user modification of the GNOME3 Login Warning Banner Text ansible.builtin.lineinfile: path: /etc/dconf/db/distro.d/locks/00-security-settings-lock regexp: ^/org/gnome/login-screen/banner-message-text$ line: /org/gnome/login-screen/banner-message-text create: true state: present when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - dconf_gnome_login_banner_text - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - name: Dconf Update ansible.builtin.command: dconf update when: '"gdm" in ansible_facts.packages' tags: - NIST-800-171-3.1.9 - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - dconf_gnome_login_banner_text - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy Protect Accounts by Configuring PAM PAM, or Pluggable Authentication Modules, is a system which implements modular authentication for Linux programs. PAM provides a flexible and configurable architecture for authentication, and it should be configured to minimize exposure to unnecessary risk. This section contains guidance on how to accomplish that. PAM is implemented as a set of shared objects which are loaded and invoked whenever an application wishes to authenticate a user. Typically, the application must be running as root in order to take advantage of PAM, because PAM's modules often need to be able to access sensitive stores of account information, such as /etc/shadow. Traditional privileged network listeners (e.g. sshd) or SUID programs (e.g. sudo) already meet this requirement. An SUID root application, userhelper, is provided so that programs which are not SUID or privileged themselves can still take advantage of PAM. PAM looks in the directory /etc/pam.d for application-specific configuration information. For instance, if the program login attempts to authenticate a user, then PAM's libraries follow the instructions in the file /etc/pam.d/login to determine what actions should be taken. One very important file in /etc/pam.d is /etc/pam.d/system-auth. This file, which is included by many other PAM configuration files, defines 'default' system authentication measures. Modifying this file is a good way to make far-reaching authentication changes, for instance when implementing a centralized authentication service. Be careful when making changes to PAM's configuration files. The syntax for these files is complex, and modifications can have unexpected consequences. The default configurations shipped with applications should be sufficient for most users. Running authconfig or system-config-authentication will re-write the PAM configuration files, destroying any manually made changes and replacing them with a series of system defaults. One reference to the configuration file syntax can be found at https://fossies.org/linux/Linux-PAM-docs/doc/sag/Linux-PAM_SAG.pdf. Password Hashing algorithm Specify the system default encryption algorithm for encrypting passwords. Defines the value set as ENCRYPT_METHOD in /etc/login.defs. SHA512 SHA512 SHA256 YESCRYPT SHA512|YESCRYPT Password Hashing algorithm for pam_unix.so Specify the system default encryption algorithm for encrypting passwords. Defines the hashing algorithm to be used in pam_unix.so. sha512 sha512 yescrypt remember The last n passwords for each user are saved in /etc/security/opasswd in order to force password change history and keep the user from alternating between the same password too frequently. 0 10 24 2 4 5 5 Install pam_pwquality Package The libpwquality package can be installed with the following command: $ sudo dnf install libpwquality SRG-OS-000480-GPOS-00225 5.3.1.3 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. "pwquality" enforces complex password construction configuration and has the ability to limit brute-force attacks on the system. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then if ! rpm -q --quiet "libpwquality" ; then dnf install -y "libpwquality" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_pam_pwquality_installed - name: Ensure libpwquality is installed ansible.builtin.package: name: libpwquality state: present when: '"pam" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_pam_pwquality_installed include install_libpwquality class install_libpwquality { package { 'libpwquality': ensure => 'installed', } } package --add=libpwquality [[packages]] name = "libpwquality" version = "*" package install libpwquality dnf install libpwquality Disallow Configuration to Bypass Password Requirements for Privilege Escalation Verify the operating system is not configured to bypass password requirements for privilege escalation. Check the configuration of the "/etc/pam.d/sudo" file with the following command: $ sudo grep pam_succeed_if /etc/pam.d/sudo If any occurrences of "pam_succeed_if" is returned from the command, this is a finding. IA-11 SRG-OS-000373-GPOS-00156 SRG-OS-000373-GPOS-00157 SRG-OS-000373-GPOS-00158 Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical the user re-authenticate. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then sed -i '/pam_succeed_if/d' /etc/pam.d/sudo else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-IA-11 - disallow_bypass_password_sudo - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Check for pam_succeed_if entry ansible.builtin.lineinfile: path: /etc/pam.d/sudo create: false regexp: pam_succeed_if state: absent when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-IA-11 - disallow_bypass_password_sudo - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Displays Last Logon/Access Notification To configure the system to notify users of last logon/access using pam_lastlog, add or correct the pam_lastlog settings in /etc/pam.d/postlogin to include showfailed option, such as: session [default=1] pam_lastlog.so showfailed And make sure that the silent option is not set for this specific line. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. authselect contains an authselect feature to easily and properly enable Last Logon notifications with pam_lastlog.so module. If a custom profile was created and used in the system before this authselect feature was available, the new feature can't be used with this custom profile and the remediation will fail. In this case, the custom profile should be recreated or manually updated. 1 12 15 16 5.5.2 DSS05.04 DSS05.10 DSS06.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 0582 0584 05885 0586 0846 0957 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-9 AC-9(1) PR.AC-7 Req-10.2.4 SRG-OS-000480-GPOS-00227 10.2.1.4 10.2.1 10.2 Users need to be aware of activity that occurs regarding their account. Providing users with information regarding the number of unsuccessful attempts that were made to login to their account allows the user to determine if any unauthorized activity has occurred and gives them an opportunity to notify administrators. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then if [ -f /usr/bin/authselect ]; then if authselect list-features sssd | grep -q with-silent-lastlog; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect disable-feature with-silent-lastlog authselect apply-changes -b else if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/postlogin") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b if [ -e "$PAM_FILE_PATH" ] ; then PAM_FILE_PATH="$PAM_FILE_PATH" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$PAM_FILE_PATH") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*session\s+\[default=1\]\s+pam_lastlog.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*session\s+.*\s+pam_lastlog.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*session\s+).*(\bpam_lastlog.so.*)/\1[default=1] \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^\s*session\s+.*pam_succeed_if\.so.*" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a session [default=1] pam_lastlog.so" "$PAM_FILE_PATH" else echo "session [default=1] pam_lastlog.so" >> "$PAM_FILE_PATH" fi fi fi # Check the option if ! grep -qP "^\s*session\s+\[default=1\]\s+pam_lastlog.so\s*.*\sshowfailed\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*session\s+\[default=1\]\s+pam_lastlog.so.*/ s/$/ showfailed/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$PAM_FILE_PATH was not found" >&2 fi if [ -e "$PAM_FILE_PATH" ] ; then PAM_FILE_PATH="$PAM_FILE_PATH" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$PAM_FILE_PATH") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*session\s+[default=1]\s+pam_lastlog.so\s.*\bsilent\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*session.*[default=1].*pam_lastlog.so.*)\bsilent\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$PAM_FILE_PATH was not found" >&2 fi fi else if [ -e "/etc/pam.d/postlogin" ] ; then PAM_FILE_PATH="/etc/pam.d/postlogin" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/postlogin") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*session\s+\[default=1\]\s+pam_lastlog.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*session\s+.*\s+pam_lastlog.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*session\s+).*(\bpam_lastlog.so.*)/\1[default=1] \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^\s*session\s+.*pam_succeed_if\.so.*" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a session [default=1] pam_lastlog.so" "$PAM_FILE_PATH" else echo "session [default=1] pam_lastlog.so" >> "$PAM_FILE_PATH" fi fi fi # Check the option if ! grep -qP "^\s*session\s+\[default=1\]\s+pam_lastlog.so\s*.*\sshowfailed\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*session\s+\[default=1\]\s+pam_lastlog.so.*/ s/$/ showfailed/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/postlogin was not found" >&2 fi if [ -e "/etc/pam.d/postlogin" ] ; then PAM_FILE_PATH="/etc/pam.d/postlogin" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/postlogin") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*session\s+[default=1]\s+pam_lastlog.so\s.*\bsilent\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*session.*[default=1].*pam_lastlog.so.*)\bsilent\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/postlogin was not found" >&2 fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2 - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - PCI-DSS-Req-10.2.4 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.4 - configure_strategy - display_login_attempts - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Ensure PAM Displays Last Logon/Access Notification - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.2 - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - PCI-DSS-Req-10.2.4 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.4 - configure_strategy - display_login_attempts - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Ensure PAM Displays Last Logon/Access Notification - Collect the Available authselect Features ansible.builtin.command: cmd: authselect list-features sssd register: result_authselect_available_features changed_when: false check_mode: false when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - CJIS-5.5.2 - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - PCI-DSS-Req-10.2.4 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.4 - configure_strategy - display_login_attempts - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Ensure PAM Displays Last Logon/Access Notification - Configure pam_lastlog.so Using authselect Feature block: - name: Ensure PAM Displays Last Logon/Access Notification - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Ensure PAM Displays Last Logon/Access Notification - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect Features Currently Enabled ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Ensure PAM Displays Last Logon/Access Notification - Ensure "with-silent-lastlog" Feature is Disabled Using authselect Tool ansible.builtin.command: cmd: authselect disable-feature with-silent-lastlog register: result_authselect_disable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is search("with-silent-lastlog") - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_disable_feature_cmd is not skipped - result_authselect_disable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists - result_authselect_available_features.stdout is search("with-silent-lastlog") tags: - CJIS-5.5.2 - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - PCI-DSS-Req-10.2.4 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.4 - configure_strategy - display_login_attempts - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Ensure PAM Displays Last Logon/Access Notification - Configure pam_lastlog.so in appropriate PAM files block: - name: Ensure PAM Displays Last Logon/Access Notification - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/postlogin - name: Ensure PAM Displays Last Logon/Access Notification - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect custom profile is used if authselect is present block: - name: Ensure PAM Displays Last Logon/Access Notification - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Ensure PAM Displays Last Logon/Access Notification - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Ensure PAM Displays Last Logon/Access Notification - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Ensure PAM Displays Last Logon/Access Notification - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Ensure PAM Displays Last Logon/Access Notification - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Ensure PAM Displays Last Logon/Access Notification - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Ensure PAM Displays Last Logon/Access Notification - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Ensure PAM Displays Last Logon/Access Notification - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Ensure PAM Displays Last Logon/Access Notification - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Ensure PAM Displays Last Logon/Access Notification - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '[default=1]' - name: Ensure PAM Displays Last Logon/Access Notification - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*session\s+{{ pam_module_control | regex_escape() }}\s+pam_lastlog.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Ensure PAM Displays Last Logon/Access Notification - Include or update the PAM module line in {{ pam_file_path }} block: - name: Ensure PAM Displays Last Logon/Access Notification - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*session\s+.*\s+pam_lastlog.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*session\s+).*(\bpam_lastlog.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' insertafter: ^\s*session\s+.*pam_succeed_if\.so.* line: session {{ pam_module_control }} pam_lastlog.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Ensure PAM Displays Last Logon/Access Notification - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '[default=1]' - name: Ensure PAM Displays Last Logon/Access Notification - Check if the required PAM module option is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*session\s+{{ pam_module_control | regex_escape() }}\s+pam_lastlog.so\s*.*\sshowfailed\b state: absent check_mode: true changed_when: false register: result_pam_module_display_login_attempts_option_present - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the "showfailed" PAM option for "pam_lastlog.so" is included in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*session\s+{{ pam_module_control | regex_escape() }}\s+pam_lastlog.so.*) line: \1 showfailed state: present register: result_pam_display_login_attempts_add when: - result_pam_module_display_login_attempts_option_present.found is defined - result_pam_module_display_login_attempts_option_present.found == 0 - name: Ensure PAM Displays Last Logon/Access Notification - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '[default=1]' - name: Ensure PAM Displays Last Logon/Access Notification - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the "silent" option from "pam_lastlog.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*session.*{{ pam_module_control | regex_escape() }}.*pam_lastlog.so.*)\bsilent\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.2 - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - PCI-DSS-Req-10.2.4 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.4 - configure_strategy - display_login_attempts - low_complexity - low_disruption - low_severity - no_reboot_needed Set Up a Private Namespace in PAM Configuration To setup a private namespace add the following line to /etc/pam.d/login: session required pam_namespace.so R55 The pam_namespace PAM module sets up a private namespace for a session with polyinstantiated directories. A polyinstantiated directory provides a different instance of itself based on user name, or when using SELinux, user name, security context or both. The polyinstatied directories can be used to dedicate separate temporary directories to each account. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then if ! grep -Eq '^\s*session\s+required\s+pam_namespace.so\s*$' '/etc/pam.d/login' ; then echo "session required pam_namespace.so" >> "/etc/pam.d/login" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_pam_namespace - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Make changes to /etc/pam.d/login ansible.builtin.lineinfile: path: /etc/pam.d/login create: false regexp: ^\s*session\s+required\s+pam_namespace.so\s*$ line: session required pam_namespace.so state: present when: '"pam" in ansible_facts.packages' tags: - enable_pam_namespace - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy Set Lockouts for Failed Password Attempts The pam_faillock PAM module provides the capability to lock out user accounts after a number of failed login attempts. Its documentation is available in /usr/share/doc/pam-VERSION/txts/README.pam_faillock. Locking out user accounts presents the risk of a denial-of-service attack. The lockout policy must weigh whether the risk of such a denial-of-service attack outweighs the benefits of thwarting password guessing attacks. fail_deny Number of failed login attempts before account lockout 10 3 4 5 6 8 3 faillock directory The directory where the user files with the failure records are kept /var/log/faillock /var/log/faillock /var/run/faillock fail_interval Interval for counting failed login attempts before account lockout 100000000 1800 3600 86400 900 900 fail_unlock_time Seconds before automatic unlocking or permanently locking after excessive failed logins 1800 3600 600 604800 86400 900 300 0 0 pwhistory_remember Prevent password re-use using password history lookup 0 1 2 3 4 5 6 7 8 9 24 5 PAM pwhistory remember - control flag 'Specify the control flag required for password remember requirement. If multiple values are allowed write them separated by commas as in "required,requisite", for remediations the first value will be taken' required optional requisite sufficient binding required,requisite requisite,required requisite Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. The pam_faillock.so module must be loaded in preauth in /etc/pam.d/password-auth. AC-7 (a) SRG-OS-000021-GPOS-00005 5.3.2.2 If the pam_faillock.so module is not loaded the system will not correctly lockout accounts to prevent password guessing attacks. if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present tags: - NIST-800-53-AC-7 (a) - account_password_pam_faillock_password_auth - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Remediation where authselect tool is present block: - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - account_password_pam_faillock_password_auth - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Remediation where authselect tool is not present block: - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth File. - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: not result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - account_password_pam_faillock_password_auth - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. The pam_faillock.so module must be loaded in preauth in /etc/pam.d/system-auth. AC-7 (a) SRG-OS-000021-GPOS-00005 5.3.2.2 If the pam_faillock.so module is not loaded the system will not correctly lockout accounts to prevent password guessing attacks. if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present tags: - NIST-800-53-AC-7 (a) - account_password_pam_faillock_system_auth - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Remediation where authselect tool is present block: - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - account_password_pam_faillock_system_auth - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Remediation where authselect tool is not present block: - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth File. - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: not result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - account_password_pam_faillock_system_auth - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed An SELinux Context must be configured for the pam_faillock.so records directory The dir configuration option in PAM pam_faillock.so module defines where the lockout records is stored. The configured directory must have the correct SELinux context. AC-7 (a) SRG-OS-000021-GPOS-00005 Not having the correct SELinux context on the pam_faillock.so records directory may lead to unauthorized access to the directory. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then FAILLOCK_CONF_FILES="/etc/security/faillock.conf /etc/pam.d/system-auth /etc/pam.d/password-auth" faillock_dirs=$(grep -oP "^\s*(?:auth.*pam_faillock.so.*)?dir\s*=\s*(\S+)" $FAILLOCK_CONF_FILES \ | sed -r 's/.*=\s*(\S+)/\1/') # Workaround for https://github.com/OpenSCAP/openscap/issues/2242: Use full # path to semanage and restorecon commands to avoid the issue with the command # not being found. if [ -n "$faillock_dirs" ]; then for dir in $faillock_dirs; do if ! /usr/sbin/semanage fcontext -a -t faillog_t "$dir(/.*)?"; then /usr/sbin/semanage fcontext -m -t faillog_t "$dir(/.*)?" fi if [ ! -e $dir ]; then mkdir -p $dir fi /usr/sbin/restorecon -R -v $dir done else echo " The pam_faillock.so dir option is not set in the system. If this is not expected, make sure pam_faillock.so is properly configured." fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-7 (a) - account_password_selinux_faillock_dir - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: An SELinux Context must be configured for the pam_faillock.so records directory - Get directories from faillock ansible.builtin.shell: grep -oP '^\s*(?:auth.*pam_faillock.so.*)?dir\s*=\s*(\S+)' "{{ item }}" | sed -r 's/.*=\s*(\S+)/\1/' register: faillock_output with_items: - /etc/security/faillock.conf - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-7 (a) - account_password_selinux_faillock_dir - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: An SELinux Context must be configured for the pam_faillock.so records directory - Create a list directories from faillock ansible.builtin.set_fact: list_faillock_dir: '{{ faillock_output.results | map(attribute=''stdout_lines'') | flatten }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-7 (a) - account_password_selinux_faillock_dir - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: An SELinux Context must be configured for the pam_faillock.so records directory - Create directories for faillock ansible.builtin.file: path: '{{ item }}' state: directory with_items: '{{ list_faillock_dir }}' when: - '"kernel" in ansible_facts.packages' - item != "" tags: - NIST-800-53-AC-7 (a) - account_password_selinux_faillock_dir - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: An SELinux Context must be configured for the pam_faillock.so records directory - Set up SELinux context for faillock ansible.builtin.shell: |- if ! semanage fcontext -a -t faillog_t "{{ item }}(/.*)?"; then semanage fcontext -m -t faillog_t "{{ item }}(/.*)?" fi with_items: '{{ list_faillock_dir }}' when: - '"kernel" in ansible_facts.packages' - item != "" tags: - NIST-800-53-AC-7 (a) - account_password_selinux_faillock_dir - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: An SELinux Context must be configured for the pam_faillock.so records directory - Restore SELinux context ansible.builtin.command: restorecon -R -v "{{ item }}" with_items: '{{ list_faillock_dir }}' when: - '"kernel" in ansible_facts.packages' - item != "" tags: - NIST-800-53-AC-7 (a) - account_password_selinux_faillock_dir - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: An SELinux Context must be configured for the pam_faillock.so records directory - Verify pam_faillock.so configuration ansible.builtin.debug: msg: |- "The pam_faillock.so dir option is not set in the system. If this is not expected, make sure pam_faillock.so is properly configured." when: - '"kernel" in ansible_facts.packages' - list_faillock_dir | length == 0 tags: - NIST-800-53-AC-7 (a) - account_password_selinux_faillock_dir - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Account Lockouts Must Be Logged PAM faillock locks an account due to excessive password failures, this event must be logged. This rule is deprecated in favor of the accounts_passwords_pam_faillock_audit rule.Please consider replacing this rule in your files as it is not expected to receive updates as of version 0.1.65. AC-7 (a) Without auditing of these events it may be harder or impossible to identify what an attacker did after an attack. if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*audit" line="audit" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\baudit\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\baudit\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*audit' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ audit/' "$pam_file" fi done fi - name: Account Lockouts Must Be Logged - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present tags: - NIST-800-53-AC-7 (a) - account_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Remediation where authselect tool is present block: - name: Account Lockouts Must Be Logged - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Account Lockouts Must Be Logged - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Account Lockouts Must Be Logged - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Account Lockouts Must Be Logged - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - account_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Remediation where authselect tool is not present block: - name: Account Lockouts Must Be Logged - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Account Lockouts Must Be Logged - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Account Lockouts Must Be Logged - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Account Lockouts Must Be Logged - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: not result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - account_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check tags: - NIST-800-53-AC-7 (a) - account_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*audit line: audit state: present when: result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7 (a) - account_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter not in PAM files block: - name: Account Lockouts Must Be Logged - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Account Lockouts Must Be Logged - Check the proper remediation for the system block: - name: Account Lockouts Must Be Logged - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Account Lockouts Must Be Logged - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Account Lockouts Must Be Logged - Ensure authselect custom profile is used if authselect is present block: - name: Account Lockouts Must Be Logged - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Account Lockouts Must Be Logged - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Account Lockouts Must Be Logged - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Account Lockouts Must Be Logged - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Account Lockouts Must Be Logged - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Account Lockouts Must Be Logged - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Account Lockouts Must Be Logged - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Account Lockouts Must Be Logged - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Account Lockouts Must Be Logged - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Account Lockouts Must Be Logged - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Account Lockouts Must Be Logged - Check the proper remediation for the system block: - name: Account Lockouts Must Be Logged - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Account Lockouts Must Be Logged - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Account Lockouts Must Be Logged - Ensure authselect custom profile is used if authselect is present block: - name: Account Lockouts Must Be Logged - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Account Lockouts Must Be Logged - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Account Lockouts Must Be Logged - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Account Lockouts Must Be Logged - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Account Lockouts Must Be Logged - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Account Lockouts Must Be Logged - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Account Lockouts Must Be Logged - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Account Lockouts Must Be Logged - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Account Lockouts Must Be Logged - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7 (a) - account_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter in PAM files block: - name: Account Lockouts Must Be Logged - Check if pam_faillock.so audit parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*audit state: absent check_mode: true changed_when: false register: result_pam_faillock_audit_parameter_is_present - name: Account Lockouts Must Be Logged - Ensure the inclusion of pam_faillock.so preauth audit parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 audit state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_audit_parameter_is_present.found == 0 when: not result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7 (a) - account_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Account Lockouts Must Persist By setting a `dir` in the faillock configuration account lockouts will persist across reboots. This rule is deprecated in favor of the accounts_passwords_pam_faillock_dir rule.Please consider replacing this rule in your files as it is not expected to receive updates as of version 0.1.65. AC-7 (ia) Having lockouts persist across reboots ensures that account is only unlocked by an administrator. If the lockouts did not persist across reboots an attack could simply reboot the system to continue brute force attacks against the accounts on the system. Limit Password Reuse: password-auth Do not allow users to reuse recent passwords. This can be accomplished by using the remember option for the pam_pwhistory PAM module. On systems with newer versions of authselect, the pam_pwhistory PAM module can be enabled via authselect feature: authselect enable-feature with-pwhistory Otherwise, it should be enabled using an authselect custom profile. Newer systems also have the /etc/security/pwhistory.conf file for setting pam_pwhistory module options. This file should be used whenever available. Otherwise, the pam_pwhistory module options can be set in PAM files. The value for remember option must be equal or greater than If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. Newer versions of authselect contain an authselect feature to easily and properly enable pam_pwhistory.so module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files. If a custom profile was created and used in the system before this authselect feature was available, the new feature can't be used with this custom profile and the remediation will fail. In this case, the custom profile should be recreated or manually updated. 1 12 15 16 5 5.6.2.1.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.8 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(f) IA-5(1)(e) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.5 SRG-OS-000077-GPOS-00045 5.3.3.3.1 8.3.7 8.3 Preventing re-use of previous passwords helps ensure that a compromised password is not re-used by a user. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_password_pam_remember='' var_password_pam_remember_control_flag='' var_password_pam_remember_control_flag="$(echo $var_password_pam_remember_control_flag | cut -d \, -f 1)" if [ -f /usr/bin/authselect ]; then if authselect list-features sssd | grep -q with-pwhistory; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-pwhistory authselect apply-changes -b else if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password $var_password_pam_remember_control_flag pam_pwhistory.so" "$PAM_FILE_PATH" else echo "password $var_password_pam_remember_control_flag pam_pwhistory.so" >> "$PAM_FILE_PATH" fi fi fi fi else if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "/etc/pam.d/password-auth"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "/etc/pam.d/password-auth")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "/etc/pam.d/password-auth" else LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "/etc/pam.d/password-auth" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password $var_password_pam_remember_control_flag pam_pwhistory.so" "/etc/pam.d/password-auth" else echo "password $var_password_pam_remember_control_flag pam_pwhistory.so" >> "/etc/pam.d/password-auth" fi fi fi fi PWHISTORY_CONF="/etc/security/pwhistory.conf" if [ -f $PWHISTORY_CONF ]; then regex="^\s*remember\s*=" line="remember = $var_password_pam_remember" if ! grep -q $regex $PWHISTORY_CONF; then echo $line >> $PWHISTORY_CONF else sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_remember"'|g' $PWHISTORY_CONF fi if [ -e "/etc/pam.d/password-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/password-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/password-auth was not found" >&2 fi else PAM_FILE_PATH="/etc/pam.d/password-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1requisite \2/" "$PAM_FILE_PATH" else echo "password requisite pam_pwhistory.so" >> "$PAM_FILE_PATH" fi fi # Check the option if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*\sremember\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+requisite\s+pam_pwhistory.so.*/ s/$/ remember=$var_password_pam_remember/" "$PAM_FILE_PATH" else sed -i -E --follow-symlinks "s/(\s*password\s+requisite\s+pam_pwhistory.so\s+.*)(remember=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_remember \3/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_password_pam_remember # promote to variable set_fact: var_password_pam_remember: !!str tags: - always - name: XCCDF Value var_password_pam_remember_control_flag # promote to variable set_fact: var_password_pam_remember_control_flag: !!str tags: - always - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: password-auth - Collect the available authselect features' ansible.builtin.command: cmd: authselect list-features sssd register: result_authselect_available_features changed_when: false check_mode: false when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: password-auth - Enable pam_pwhistory.so using authselect feature' block: - name: 'Limit Password Reuse: password-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: password-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: password-auth - Get authselect current features' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: password-auth - Ensure "with-pwhistory" feature is enabled using authselect tool' ansible.builtin.command: cmd: authselect enable-feature with-pwhistory register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-pwhistory") - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists - result_authselect_available_features.stdout is search("with-pwhistory") tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: password-auth - Enable pam_pwhistory.so in appropriate PAM files' block: - name: 'Limit Password Reuse: password-auth - Define the PAM file to be edited as a local fact' ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: 'Limit Password Reuse: password-auth - Ensure authselect custom profile is used if authselect is present' block: - name: 'Limit Password Reuse: password-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: password-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: password-auth - Get authselect current profile' ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: password-auth - Define the current authselect profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: 'Limit Password Reuse: password-auth - Define the new authselect custom profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: 'Limit Password Reuse: password-auth - Get authselect current features to also enable them in the custom profile' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: password-auth - Check if any custom profile with the same name was already created' ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile based on the current profile' ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile based on sssd profile' ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: password-auth - Ensure the authselect custom profile is selected' ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: password-auth - Restore the authselect features in the custom profile' ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: 'Limit Password Reuse: password-auth - Change the PAM file to be edited according to the custom authselect profile' ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: 'Limit Password Reuse: password-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: '{{ var_password_pam_remember_control_flag.split(",")[0] }}' - name: 'Limit Password Reuse: password-auth - Check if expected PAM module line is present in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: 'Limit Password Reuse: password-auth - Include or update the PAM module line in {{ pam_file_path }}' block: - name: 'Limit Password Reuse: password-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: 'Limit Password Reuse: password-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }}' ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: 'Limit Password Reuse: password-auth - Ensure the required PAM module line is included in {{ pam_file_path }}' ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' insertafter: ^password.*requisite.*pam_pwquality\.so line: password {{ pam_module_control }} pam_pwhistory.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 when: - '"pam" in ansible_facts.packages' - | (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: password-auth - Check the presence of /etc/security/pwhistory.conf file' ansible.builtin.stat: path: /etc/security/pwhistory.conf register: result_pwhistory_conf_check when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: password-auth - pam_pwhistory.so parameters are configured in /etc/security/pwhistory.conf file' block: - name: 'Limit Password Reuse: password-auth - Ensure the pam_pwhistory.so remember parameter in /etc/security/pwhistory.conf' ansible.builtin.lineinfile: path: /etc/security/pwhistory.conf regexp: ^\s*remember\s*= line: remember = {{ var_password_pam_remember }} state: present - name: 'Limit Password Reuse: password-auth - Ensure the pam_pwhistory.so remember parameter is removed from PAM files' block: - name: 'Limit Password Reuse: password-auth - Check if /etc/pam.d/password-auth file is present' ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: 'Limit Password Reuse: password-auth - Check the proper remediation for the system' block: - name: 'Limit Password Reuse: password-auth - Define the PAM file to be edited as a local fact' ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: 'Limit Password Reuse: password-auth - Ensure authselect custom profile is used if authselect is present' block: - name: 'Limit Password Reuse: password-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: password-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: password-auth - Get authselect current profile' ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: password-auth - Define the current authselect profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: 'Limit Password Reuse: password-auth - Define the new authselect custom profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: 'Limit Password Reuse: password-auth - Get authselect current features to also enable them in the custom profile' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: password-auth - Check if any custom profile with the same name was already created' ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile based on the current profile' ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile based on sssd profile' ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: password-auth - Ensure the authselect custom profile is selected' ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: password-auth - Restore the authselect features in the custom profile' ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: 'Limit Password Reuse: password-auth - Change the PAM file to be edited according to the custom authselect profile' ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: 'Limit Password Reuse: password-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: '' - name: 'Limit Password Reuse: password-auth - Check if {{ pam_file_path }} file is present' ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: 'Limit Password Reuse: password-auth - Ensure the "remember" option from "pam_pwhistory.so" is not present in {{ pam_file_path }}' ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*password.*pam_pwhistory.so.*)\bremember\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_pwhistory_conf_check.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: password-auth - pam_pwhistory.so parameters are configured in PAM files' block: - name: 'Limit Password Reuse: password-auth - Define the PAM file to be edited as a local fact' ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: 'Limit Password Reuse: password-auth - Ensure authselect custom profile is used if authselect is present' block: - name: 'Limit Password Reuse: password-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: password-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: password-auth - Get authselect current profile' ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: password-auth - Define the current authselect profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: 'Limit Password Reuse: password-auth - Define the new authselect custom profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: 'Limit Password Reuse: password-auth - Get authselect current features to also enable them in the custom profile' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: password-auth - Check if any custom profile with the same name was already created' ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile based on the current profile' ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile based on sssd profile' ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: password-auth - Ensure the authselect custom profile is selected' ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: password-auth - Restore the authselect features in the custom profile' ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: 'Limit Password Reuse: password-auth - Change the PAM file to be edited according to the custom authselect profile' ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: 'Limit Password Reuse: password-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: requisite - name: 'Limit Password Reuse: password-auth - Check if expected PAM module line is present in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: 'Limit Password Reuse: password-auth - Include or update the PAM module line in {{ pam_file_path }}' block: - name: 'Limit Password Reuse: password-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: 'Limit Password Reuse: password-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }}' ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: 'Limit Password Reuse: password-auth - Ensure the required PAM module line is included in {{ pam_file_path }}' ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' line: password {{ pam_module_control }} pam_pwhistory.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: 'Limit Password Reuse: password-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: requisite - name: 'Limit Password Reuse: password-auth - Check if the required PAM module option is present in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*\sremember\b state: absent check_mode: true changed_when: false register: result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present - name: 'Limit Password Reuse: password-auth - Ensure the "remember" PAM option for "pam_pwhistory.so" is included in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so.*) line: \1 remember={{ var_password_pam_remember }} state: present register: result_pam_accounts_password_pam_pwhistory_remember_password_auth_add when: - result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present.found is defined - result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present.found == 0 - name: 'Limit Password Reuse: password-auth - Ensure the required value for "remember" PAM option from "pam_pwhistory.so" in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s+.*)(remember)=[0-9a-zA-Z]*\s*(.*) line: \1\2={{ var_password_pam_remember }} \3 register: result_pam_accounts_password_pam_pwhistory_remember_password_auth_edit when: - result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present.found > 0 - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - (result_pam_remember_add is defined and result_pam_remember_add.changed) or (result_pam_remember_edit is defined and result_pam_remember_edit.changed) when: - '"pam" in ansible_facts.packages' - not result_pwhistory_conf_check.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed Limit Password Reuse: system-auth Do not allow users to reuse recent passwords. This can be accomplished by using the remember option for the pam_pwhistory PAM module. On systems with newer versions of authselect, the pam_pwhistory PAM module can be enabled via authselect feature: authselect enable-feature with-pwhistory Otherwise, it should be enabled using an authselect custom profile. Newer systems also have the /etc/security/pwhistory.conf file for setting pam_pwhistory module options. This file should be used whenever available. Otherwise, the pam_pwhistory module options can be set in PAM files. The value for remember option must be equal or greater than If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. Newer versions of authselect contain an authselect feature to easily and properly enable pam_pwhistory.so module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files. 1 12 15 16 5 5.6.2.1.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.8 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(f) IA-5(1)(e) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.5 SRG-OS-000077-GPOS-00045 5.3.3.3.1 8.3.7 8.3 Preventing re-use of previous passwords helps ensure that a compromised password is not re-used by a user. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_password_pam_remember='' var_password_pam_remember_control_flag='' var_password_pam_remember_control_flag="$(echo $var_password_pam_remember_control_flag | cut -d \, -f 1)" if [ -f /usr/bin/authselect ]; then if authselect list-features sssd | grep -q with-pwhistory; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-pwhistory authselect apply-changes -b else if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password $var_password_pam_remember_control_flag pam_pwhistory.so" "$PAM_FILE_PATH" else echo "password $var_password_pam_remember_control_flag pam_pwhistory.so" >> "$PAM_FILE_PATH" fi fi fi fi else if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "/etc/pam.d/system-auth"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "/etc/pam.d/system-auth")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "/etc/pam.d/system-auth" else LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "/etc/pam.d/system-auth" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password $var_password_pam_remember_control_flag pam_pwhistory.so" "/etc/pam.d/system-auth" else echo "password $var_password_pam_remember_control_flag pam_pwhistory.so" >> "/etc/pam.d/system-auth" fi fi fi fi PWHISTORY_CONF="/etc/security/pwhistory.conf" if [ -f $PWHISTORY_CONF ]; then regex="^\s*remember\s*=" line="remember = $var_password_pam_remember" if ! grep -q $regex $PWHISTORY_CONF; then echo $line >> $PWHISTORY_CONF else sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_remember"'|g' $PWHISTORY_CONF fi if [ -e "/etc/pam.d/system-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/system-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/system-auth was not found" >&2 fi else PAM_FILE_PATH="/etc/pam.d/system-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1requisite \2/" "$PAM_FILE_PATH" else echo "password requisite pam_pwhistory.so" >> "$PAM_FILE_PATH" fi fi # Check the option if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*\sremember\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+requisite\s+pam_pwhistory.so.*/ s/$/ remember=$var_password_pam_remember/" "$PAM_FILE_PATH" else sed -i -E --follow-symlinks "s/(\s*password\s+requisite\s+pam_pwhistory.so\s+.*)(remember=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_remember \3/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_password_pam_remember # promote to variable set_fact: var_password_pam_remember: !!str tags: - always - name: XCCDF Value var_password_pam_remember_control_flag # promote to variable set_fact: var_password_pam_remember_control_flag: !!str tags: - always - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: system-auth - Collect the available authselect features' ansible.builtin.command: cmd: authselect list-features sssd register: result_authselect_available_features changed_when: false check_mode: false when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: system-auth - Enable pam_pwhistory.so using authselect feature' block: - name: 'Limit Password Reuse: system-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: system-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: system-auth - Get authselect current features' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: system-auth - Ensure "with-pwhistory" feature is enabled using authselect tool' ansible.builtin.command: cmd: authselect enable-feature with-pwhistory register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-pwhistory") - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists - result_authselect_available_features.stdout is search("with-pwhistory") tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: system-auth - Enable pam_pwhistory.so in appropriate PAM files' block: - name: 'Limit Password Reuse: system-auth - Define the PAM file to be edited as a local fact' ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: 'Limit Password Reuse: system-auth - Ensure authselect custom profile is used if authselect is present' block: - name: 'Limit Password Reuse: system-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: system-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: system-auth - Get authselect current profile' ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: system-auth - Define the current authselect profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: 'Limit Password Reuse: system-auth - Define the new authselect custom profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: 'Limit Password Reuse: system-auth - Get authselect current features to also enable them in the custom profile' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: system-auth - Check if any custom profile with the same name was already created' ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile based on the current profile' ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile based on sssd profile' ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: system-auth - Ensure the authselect custom profile is selected' ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: system-auth - Restore the authselect features in the custom profile' ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: 'Limit Password Reuse: system-auth - Change the PAM file to be edited according to the custom authselect profile' ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: 'Limit Password Reuse: system-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: '{{ var_password_pam_remember_control_flag.split(",")[0] }}' - name: 'Limit Password Reuse: system-auth - Check if expected PAM module line is present in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: 'Limit Password Reuse: system-auth - Include or update the PAM module line in {{ pam_file_path }}' block: - name: 'Limit Password Reuse: system-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: 'Limit Password Reuse: system-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }}' ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: 'Limit Password Reuse: system-auth - Ensure the required PAM module line is included in {{ pam_file_path }}' ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' insertafter: ^password.*requisite.*pam_pwquality\.so line: password {{ pam_module_control }} pam_pwhistory.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 when: - '"pam" in ansible_facts.packages' - | (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: system-auth - Check the presence of /etc/security/pwhistory.conf file' ansible.builtin.stat: path: /etc/security/pwhistory.conf register: result_pwhistory_conf_check when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: system-auth - pam_pwhistory.so parameters are configured in /etc/security/pwhistory.conf file' block: - name: 'Limit Password Reuse: system-auth - Ensure the pam_pwhistory.so remember parameter in /etc/security/pwhistory.conf' ansible.builtin.lineinfile: path: /etc/security/pwhistory.conf regexp: ^\s*remember\s*= line: remember = {{ var_password_pam_remember }} state: present - name: 'Limit Password Reuse: system-auth - Ensure the pam_pwhistory.so remember parameter is removed from PAM files' block: - name: 'Limit Password Reuse: system-auth - Check if /etc/pam.d/system-auth file is present' ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: 'Limit Password Reuse: system-auth - Check the proper remediation for the system' block: - name: 'Limit Password Reuse: system-auth - Define the PAM file to be edited as a local fact' ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: 'Limit Password Reuse: system-auth - Ensure authselect custom profile is used if authselect is present' block: - name: 'Limit Password Reuse: system-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: system-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: system-auth - Get authselect current profile' ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: system-auth - Define the current authselect profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: 'Limit Password Reuse: system-auth - Define the new authselect custom profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: 'Limit Password Reuse: system-auth - Get authselect current features to also enable them in the custom profile' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: system-auth - Check if any custom profile with the same name was already created' ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile based on the current profile' ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile based on sssd profile' ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: system-auth - Ensure the authselect custom profile is selected' ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: system-auth - Restore the authselect features in the custom profile' ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: 'Limit Password Reuse: system-auth - Change the PAM file to be edited according to the custom authselect profile' ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: 'Limit Password Reuse: system-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: '' - name: 'Limit Password Reuse: system-auth - Check if {{ pam_file_path }} file is present' ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: 'Limit Password Reuse: system-auth - Ensure the "remember" option from "pam_pwhistory.so" is not present in {{ pam_file_path }}' ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*password.*pam_pwhistory.so.*)\bremember\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_pwhistory_conf_check.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: 'Limit Password Reuse: system-auth - pam_pwhistory.so parameters are configured in PAM files' block: - name: 'Limit Password Reuse: system-auth - Define the PAM file to be edited as a local fact' ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect tool' ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: 'Limit Password Reuse: system-auth - Ensure authselect custom profile is used if authselect is present' block: - name: 'Limit Password Reuse: system-auth - Check integrity of authselect current profile' ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: 'Limit Password Reuse: system-auth - Informative message based on the authselect integrity check result' ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: 'Limit Password Reuse: system-auth - Get authselect current profile' ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: 'Limit Password Reuse: system-auth - Define the current authselect profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: 'Limit Password Reuse: system-auth - Define the new authselect custom profile as a local fact' ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: 'Limit Password Reuse: system-auth - Get authselect current features to also enable them in the custom profile' ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: system-auth - Check if any custom profile with the same name was already created' ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile based on the current profile' ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile based on sssd profile' ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: system-auth - Ensure the authselect custom profile is selected' ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: 'Limit Password Reuse: system-auth - Restore the authselect features in the custom profile' ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: 'Limit Password Reuse: system-auth - Change the PAM file to be edited according to the custom authselect profile' ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: 'Limit Password Reuse: system-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: requisite - name: 'Limit Password Reuse: system-auth - Check if expected PAM module line is present in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: 'Limit Password Reuse: system-auth - Include or update the PAM module line in {{ pam_file_path }}' block: - name: 'Limit Password Reuse: system-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: 'Limit Password Reuse: system-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }}' ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: 'Limit Password Reuse: system-auth - Ensure the required PAM module line is included in {{ pam_file_path }}' ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' line: password {{ pam_module_control }} pam_pwhistory.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: 'Limit Password Reuse: system-auth - Define a fact for control already filtered in case filters are used' ansible.builtin.set_fact: pam_module_control: requisite - name: 'Limit Password Reuse: system-auth - Check if the required PAM module option is present in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*\sremember\b state: absent check_mode: true changed_when: false register: result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present - name: 'Limit Password Reuse: system-auth - Ensure the "remember" PAM option for "pam_pwhistory.so" is included in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so.*) line: \1 remember={{ var_password_pam_remember }} state: present register: result_pam_accounts_password_pam_pwhistory_remember_system_auth_add when: - result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present.found is defined - result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present.found == 0 - name: 'Limit Password Reuse: system-auth - Ensure the required value for "remember" PAM option from "pam_pwhistory.so" in {{ pam_file_path }}' ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s+.*)(remember)=[0-9a-zA-Z]*\s*(.*) line: \1\2={{ var_password_pam_remember }} \3 register: result_pam_accounts_password_pam_pwhistory_remember_system_auth_edit when: - result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present.found > 0 - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied' ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - (result_pam_remember_add is defined and result_pam_remember_add.changed) or (result_pam_remember_edit is defined and result_pam_remember_edit.changed) when: - '"pam" in ansible_facts.packages' - not result_pwhistory_conf_check.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_pwhistory_remember_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed Limit Password Reuse Do not allow users to reuse recent passwords. This can be accomplished by using the remember option for the pam_unix or pam_pwhistory PAM modules. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. Newer versions of authselect contain an authselect feature to easily and properly enable pam_pwhistory.so module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files. 1 12 15 16 5 5.6.2.1.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.8 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(f) IA-5(1)(e) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.5 SRG-OS-000077-GPOS-00045 R31 8.3.7 8.3 Preventing re-use of previous passwords helps ensure that a compromised password is not re-used by a user. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_unix_remember='' if [ -f /usr/bin/authselect ]; then if authselect list-features sssd | grep -q with-pwhistory; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-pwhistory authselect apply-changes -b else if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1requisite \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password requisite pam_pwhistory.so" "$PAM_FILE_PATH" else echo "password requisite pam_pwhistory.so" >> "$PAM_FILE_PATH" fi fi fi fi else if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*" "/etc/pam.d/system-auth"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "/etc/pam.d/system-auth")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1requisite \2/" "/etc/pam.d/system-auth" else LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "/etc/pam.d/system-auth" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password requisite pam_pwhistory.so" "/etc/pam.d/system-auth" else echo "password requisite pam_pwhistory.so" >> "/etc/pam.d/system-auth" fi fi fi fi PWHISTORY_CONF="/etc/security/pwhistory.conf" if [ -f $PWHISTORY_CONF ]; then regex="^\s*remember\s*=" line="remember = $var_password_pam_unix_remember" if ! grep -q $regex $PWHISTORY_CONF; then echo $line >> $PWHISTORY_CONF else sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_unix_remember"'|g' $PWHISTORY_CONF fi if [ -e "/etc/pam.d/system-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/system-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/system-auth was not found" >&2 fi else PAM_FILE_PATH="/etc/pam.d/system-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1requisite \2/" "$PAM_FILE_PATH" else echo "password requisite pam_pwhistory.so" >> "$PAM_FILE_PATH" fi fi # Check the option if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*\sremember\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+requisite\s+pam_pwhistory.so.*/ s/$/ remember=$var_password_pam_unix_remember/" "$PAM_FILE_PATH" else sed -i -E --follow-symlinks "s/(\s*password\s+requisite\s+pam_pwhistory.so\s+.*)(remember=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_unix_remember \3/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_password_pam_unix_remember # promote to variable set_fact: var_password_pam_unix_remember: !!str tags: - always - name: Limit Password Reuse - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Limit Password Reuse - Collect the available authselect features ansible.builtin.command: cmd: authselect list-features sssd register: result_authselect_available_features changed_when: false check_mode: false when: - '"libpwquality" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Limit Password Reuse - Enable pam_pwhistory.so using authselect feature block: - name: Limit Password Reuse - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Limit Password Reuse - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Limit Password Reuse - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Limit Password Reuse - Ensure "with-pwhistory" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-pwhistory register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-pwhistory") - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"libpwquality" in ansible_facts.packages' - result_authselect_present.stat.exists - result_authselect_available_features.stdout is search("with-pwhistory") tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Limit Password Reuse - Enable pam_pwhistory.so in appropriate PAM files block: - name: Limit Password Reuse - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Limit Password Reuse - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Limit Password Reuse - Ensure authselect custom profile is used if authselect is present block: - name: Limit Password Reuse - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Limit Password Reuse - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Limit Password Reuse - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Limit Password Reuse - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Limit Password Reuse - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Limit Password Reuse - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Limit Password Reuse - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Limit Password Reuse - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Limit Password Reuse - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Limit Password Reuse - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Limit Password Reuse - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Limit Password Reuse - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Limit Password Reuse - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: requisite - name: Limit Password Reuse - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Limit Password Reuse - Include or update the PAM module line in {{ pam_file_path }} block: - name: Limit Password Reuse - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Limit Password Reuse - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Limit Password Reuse - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' insertafter: ^password.*requisite.*pam_pwquality\.so line: password {{ pam_module_control }} pam_pwhistory.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 when: - '"libpwquality" in ansible_facts.packages' - | (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Limit Password Reuse - Check the presence of /etc/security/pwhistory.conf file ansible.builtin.stat: path: /etc/security/pwhistory.conf register: result_pwhistory_conf_check when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Limit Password Reuse - pam_pwhistory.so parameters are configured in /etc/security/pwhistory.conf file block: - name: Limit Password Reuse - Ensure the pam_pwhistory.so remember parameter in /etc/security/pwhistory.conf ansible.builtin.lineinfile: path: /etc/security/pwhistory.conf regexp: ^\s*remember\s*= line: remember = {{ var_password_pam_unix_remember }} state: present - name: Limit Password Reuse - Ensure the pam_pwhistory.so remember parameter is removed from PAM files block: - name: Limit Password Reuse - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Limit Password Reuse - Check the proper remediation for the system block: - name: Limit Password Reuse - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Limit Password Reuse - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Limit Password Reuse - Ensure authselect custom profile is used if authselect is present block: - name: Limit Password Reuse - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Limit Password Reuse - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Limit Password Reuse - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Limit Password Reuse - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Limit Password Reuse - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Limit Password Reuse - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Limit Password Reuse - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Limit Password Reuse - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Limit Password Reuse - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Limit Password Reuse - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Limit Password Reuse - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Limit Password Reuse - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Limit Password Reuse - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Limit Password Reuse - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Limit Password Reuse - Ensure the "remember" option from "pam_pwhistory.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*password.*pam_pwhistory.so.*)\bremember\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"libpwquality" in ansible_facts.packages' - result_pwhistory_conf_check.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Limit Password Reuse - pam_pwhistory.so parameters are configured in PAM files block: - name: Limit Password Reuse - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Limit Password Reuse - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Limit Password Reuse - Ensure authselect custom profile is used if authselect is present block: - name: Limit Password Reuse - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Limit Password Reuse - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Limit Password Reuse - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Limit Password Reuse - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Limit Password Reuse - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Limit Password Reuse - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Limit Password Reuse - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Limit Password Reuse - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Limit Password Reuse - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Limit Password Reuse - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Limit Password Reuse - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Limit Password Reuse - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Limit Password Reuse - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: requisite - name: Limit Password Reuse - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Limit Password Reuse - Include or update the PAM module line in {{ pam_file_path }} block: - name: Limit Password Reuse - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Limit Password Reuse - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Limit Password Reuse - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' line: password {{ pam_module_control }} pam_pwhistory.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Limit Password Reuse - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: requisite - name: Limit Password Reuse - Check if the required PAM module option is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*\sremember\b state: absent check_mode: true changed_when: false register: result_pam_module_accounts_password_pam_unix_remember_option_present - name: Limit Password Reuse - Ensure the "remember" PAM option for "pam_pwhistory.so" is included in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so.*) line: \1 remember={{ var_password_pam_unix_remember }} state: present register: result_pam_accounts_password_pam_unix_remember_add when: - result_pam_module_accounts_password_pam_unix_remember_option_present.found is defined - result_pam_module_accounts_password_pam_unix_remember_option_present.found == 0 - name: Limit Password Reuse - Ensure the required value for "remember" PAM option from "pam_pwhistory.so" in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s+.*)(remember)=[0-9a-zA-Z]*\s*(.*) line: \1\2={{ var_password_pam_unix_remember }} \3 register: result_pam_accounts_password_pam_unix_remember_edit when: - result_pam_module_accounts_password_pam_unix_remember_option_present.found > 0 - name: Limit Password Reuse - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - (result_pam_remember_add is defined and result_pam_remember_add.changed) or (result_pam_remember_edit is defined and result_pam_remember_edit.changed) when: - '"libpwquality" in ansible_facts.packages' - not result_pwhistory_conf_check.stat.exists tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-IA-5(1)(e) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.5 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.7 - accounts_password_pam_unix_remember - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed Account Lockouts Must Be Logged PAM faillock locks an account due to excessive password failures, this event must be logged. AC-7 (a) SRG-OS-000021-GPOS-00005 Without auditing of these events it may be harder or impossible to identify what an attacker did after an attack. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*audit" line="audit" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\baudit\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\baudit\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*audit' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ audit/' "$pam_file" fi done fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Remediation where authselect tool is present block: - name: Account Lockouts Must Be Logged - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Account Lockouts Must Be Logged - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Account Lockouts Must Be Logged - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Account Lockouts Must Be Logged - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Remediation where authselect tool is not present block: - name: Account Lockouts Must Be Logged - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Account Lockouts Must Be Logged - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Account Lockouts Must Be Logged - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Account Lockouts Must Be Logged - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: - '"pam" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*audit line: audit state: present when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter not in PAM files block: - name: Account Lockouts Must Be Logged - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Account Lockouts Must Be Logged - Check the proper remediation for the system block: - name: Account Lockouts Must Be Logged - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Account Lockouts Must Be Logged - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Account Lockouts Must Be Logged - Ensure authselect custom profile is used if authselect is present block: - name: Account Lockouts Must Be Logged - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Account Lockouts Must Be Logged - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Account Lockouts Must Be Logged - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Account Lockouts Must Be Logged - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Account Lockouts Must Be Logged - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Account Lockouts Must Be Logged - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Account Lockouts Must Be Logged - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Account Lockouts Must Be Logged - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Account Lockouts Must Be Logged - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Account Lockouts Must Be Logged - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Account Lockouts Must Be Logged - Check the proper remediation for the system block: - name: Account Lockouts Must Be Logged - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Account Lockouts Must Be Logged - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Account Lockouts Must Be Logged - Ensure authselect custom profile is used if authselect is present block: - name: Account Lockouts Must Be Logged - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Account Lockouts Must Be Logged - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Account Lockouts Must Be Logged - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Account Lockouts Must Be Logged - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Account Lockouts Must Be Logged - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Account Lockouts Must Be Logged - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Account Lockouts Must Be Logged - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Account Lockouts Must Be Logged - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Account Lockouts Must Be Logged - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Account Lockouts Must Be Logged - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter in PAM files block: - name: Account Lockouts Must Be Logged - Check if pam_faillock.so audit parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*audit state: absent check_mode: true changed_when: false register: result_pam_faillock_audit_parameter_is_present - name: Account Lockouts Must Be Logged - Ensure the inclusion of pam_faillock.so preauth audit parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 audit state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_audit_parameter_is_present.found == 0 when: - '"pam" in ansible_facts.packages' - not result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7 (a) - accounts_passwords_pam_faillock_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Lock Accounts After Failed Password Attempts This rule configures the system to lock out accounts after a number of incorrect login attempts using pam_faillock.so. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. Ensure that the file /etc/security/faillock.conf contains the following entry: deny = <count> Where count should be less than or equal to and greater than 0. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file. 1 12 15 16 5.5.3 DSS05.04 DSS05.10 DSS06.10 3.1.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) AC-7(a) PR.AC-7 FIA_AFL.1 Req-8.1.6 SRG-OS-000329-GPOS-00128 SRG-OS-000021-GPOS-00005 R31 5.3.3.1.1 8.3.4 8.3 By limiting the number of failed logon attempts, the risk of unauthorized system access via user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking the account. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_accounts_passwords_pam_faillock_deny='' if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*deny\s*=" line="deny = $var_accounts_passwords_pam_faillock_deny" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF else sed -i --follow-symlinks 's|^\s*\(deny\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_deny"'|g' $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\bdeny\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\bdeny\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*deny' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ deny='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ deny='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file" else sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*\)\('"deny"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_deny"'\3/' "$pam_file" sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"deny"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_deny"'\3/' "$pam_file" fi done fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Lock Accounts After Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Lock Accounts After Failed Password Attempts - Remediation where authselect tool is present block: - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Lock Accounts After Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Lock Accounts After Failed Password Attempts - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Lock Accounts After Failed Password Attempts - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Lock Accounts After Failed Password Attempts - Remediation where authselect tool is not present block: - name: Lock Accounts After Failed Password Attempts - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Lock Accounts After Failed Password Attempts - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Lock Accounts After Failed Password Attempts - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Lock Accounts After Failed Password Attempts - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: - '"pam" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_passwords_pam_faillock_deny # promote to variable set_fact: var_accounts_passwords_pam_faillock_deny: !!str tags: - always - name: Lock Accounts After Failed Password Attempts - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so deny parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*deny\s*= line: deny = {{ var_accounts_passwords_pam_faillock_deny }} state: present when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so deny parameter not in PAM files block: - name: Lock Accounts After Failed Password Attempts - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Lock Accounts After Failed Password Attempts - Check the proper remediation for the system block: - name: Lock Accounts After Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Lock Accounts After Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Lock Accounts After Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Lock Accounts After Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Lock Accounts After Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Lock Accounts After Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Lock Accounts After Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Lock Accounts After Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts After Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts After Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts After Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts After Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Lock Accounts After Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Lock Accounts After Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Lock Accounts After Failed Password Attempts - Ensure the "deny" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bdeny\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Lock Accounts After Failed Password Attempts - Check the proper remediation for the system block: - name: Lock Accounts After Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Lock Accounts After Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Lock Accounts After Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Lock Accounts After Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Lock Accounts After Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Lock Accounts After Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Lock Accounts After Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Lock Accounts After Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts After Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts After Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts After Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts After Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Lock Accounts After Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Lock Accounts After Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Lock Accounts After Failed Password Attempts - Ensure the "deny" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bdeny\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so deny parameter in PAM files block: - name: Lock Accounts After Failed Password Attempts - Check if pam_faillock.so deny parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*deny state: absent check_mode: true changed_when: false register: result_pam_faillock_deny_parameter_is_present - name: Lock Accounts After Failed Password Attempts - Ensure the inclusion of pam_faillock.so preauth deny parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 deny={{ var_accounts_passwords_pam_faillock_deny }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_deny_parameter_is_present.found == 0 - name: Lock Accounts After Failed Password Attempts - Ensure the inclusion of pam_faillock.so authfail deny parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*) line: \1required\3 deny={{ var_accounts_passwords_pam_faillock_deny }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_deny_parameter_is_present.found == 0 - name: Lock Accounts After Failed Password Attempts - Ensure the desired value for pam_faillock.so preauth deny parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(deny)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_deny }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_deny_parameter_is_present.found > 0 - name: Lock Accounts After Failed Password Attempts - Ensure the desired value for pam_faillock.so authfail deny parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(deny)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_deny }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_deny_parameter_is_present.found > 0 when: - '"pam" in ansible_facts.packages' - not result_faillock_conf_check.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.6 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_deny - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Configure the root Account for Failed Password Attempts This rule configures the system to lock out the root account after a number of incorrect login attempts using pam_faillock.so. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) AC-7(b) IA-5(c) PR.AC-7 SRG-OS-000329-GPOS-00128 SRG-OS-000021-GPOS-00005 R31 5.3.3.1.3 By limiting the number of failed logon attempts, the risk of unauthorized system access via user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking the account. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*even_deny_root" line="even_deny_root" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\beven_deny_root\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*even_deny_root' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ even_deny_root/' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ even_deny_root/' "$pam_file" fi done fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the root Account for Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the root Account for Failed Password Attempts - Remediation where authselect tool is present block: - name: Configure the root Account for Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Configure the root Account for Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Configure the root Account for Failed Password Attempts - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Configure the root Account for Failed Password Attempts - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Configure the root Account for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the root Account for Failed Password Attempts - Remediation where authselect tool is not present block: - name: Configure the root Account for Failed Password Attempts - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Configure the root Account for Failed Password Attempts - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Configure the root Account for Failed Password Attempts - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Configure the root Account for Failed Password Attempts - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: - '"pam" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the root Account for Failed Password Attempts - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so even_deny_root parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*even_deny_root line: even_deny_root state: present when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so even_deny_root parameter not in PAM files block: - name: Configure the root Account for Failed Password Attempts - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Configure the root Account for Failed Password Attempts - Check the proper remediation for the system block: - name: Configure the root Account for Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Configure the root Account for Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Configure the root Account for Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Configure the root Account for Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Configure the root Account for Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Configure the root Account for Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Configure the root Account for Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Configure the root Account for Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Configure the root Account for Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Configure the root Account for Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Configure the root Account for Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Configure the root Account for Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Configure the root Account for Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Configure the root Account for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Configure the root Account for Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Configure the root Account for Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Configure the root Account for Failed Password Attempts - Ensure the "even_deny_root" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Configure the root Account for Failed Password Attempts - Check the proper remediation for the system block: - name: Configure the root Account for Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Configure the root Account for Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Configure the root Account for Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Configure the root Account for Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Configure the root Account for Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Configure the root Account for Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Configure the root Account for Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Configure the root Account for Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Configure the root Account for Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Configure the root Account for Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Configure the root Account for Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Configure the root Account for Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Configure the root Account for Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Configure the root Account for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Configure the root Account for Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Configure the root Account for Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Configure the root Account for Failed Password Attempts - Ensure the "even_deny_root" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Configure the root Account for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so even_deny_root parameter in PAM files block: - name: Configure the root Account for Failed Password Attempts - Check if pam_faillock.so even_deny_root parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*even_deny_root state: absent check_mode: true changed_when: false register: result_pam_faillock_even_deny_root_parameter_is_present - name: Configure the root Account for Failed Password Attempts - Ensure the inclusion of pam_faillock.so preauth even_deny_root parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 even_deny_root state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_even_deny_root_parameter_is_present.found == 0 - name: Configure the root Account for Failed Password Attempts - Ensure the inclusion of pam_faillock.so authfail even_deny_root parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*) line: \1required\3 even_deny_root state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_even_deny_root_parameter_is_present.found == 0 when: - '"pam" in ansible_facts.packages' - not result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(c) - accounts_passwords_pam_faillock_deny_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Lock Accounts Must Persist This rule ensures that the system lock out accounts using pam_faillock.so persist after system reboot. From "pam_faillock" man pages: Note that the default directory that "pam_faillock" uses is usually cleared on system boot so the access will be reenabled after system reboot. If that is undesirable, a different tally directory must be set with the "dir" option. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version. The chosen profile expects the directory to be . To configure the tally directory, add the following line to /etc/security/faillock.conf: dir = If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file. AC-7(b) AC-7(a) AC-7.1(ii) SRG-OS-000021-GPOS-00005 SRG-OS-000329-GPOS-00128 Locking out user accounts after a number of incorrect attempts prevents direct password guessing attacks. In combination with the silent option, user enumeration attacks are also mitigated. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_accounts_passwords_pam_faillock_dir='' if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*dir\s*=" line="dir = $var_accounts_passwords_pam_faillock_dir" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF else sed -i --follow-symlinks 's|^\s*\(dir\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_dir"'|g' $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\bdir\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\bdir\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*dir' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ dir='"$var_accounts_passwords_pam_faillock_dir"'/' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ dir='"$var_accounts_passwords_pam_faillock_dir"'/' "$pam_file" else sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*\)\('"dir"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_dir"'\3/' "$pam_file" sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"dir"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_dir"'\3/' "$pam_file" fi done fi if ! rpm -q --quiet "python3-libselinux" ; then dnf install -y "python3-libselinux" fi if ! rpm -q --quiet "python3-policycoreutils" ; then dnf install -y "python3-policycoreutils" fi if ! rpm -q --quiet "policycoreutils-python-utils" ; then dnf install -y "policycoreutils-python-utils" fi mkdir -p "$var_accounts_passwords_pam_faillock_dir" # Workaround for https://github.com/OpenSCAP/openscap/issues/2242: Use full # path to semanage and restorecon commands to avoid the issue with the command # not being found. /usr/sbin/semanage fcontext -a -t faillog_t "$var_accounts_passwords_pam_faillock_dir(/.*)?" /usr/sbin/restorecon -R -v "$var_accounts_passwords_pam_faillock_dir" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Remediation where authselect tool is present block: - name: Lock Accounts Must Persist - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Lock Accounts Must Persist - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Lock Accounts Must Persist - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Lock Accounts Must Persist - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Lock Accounts Must Persist - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Remediation where authselect tool is not present block: - name: Lock Accounts Must Persist - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Lock Accounts Must Persist - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Lock Accounts Must Persist - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Lock Accounts Must Persist - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: - '"pam" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_accounts_passwords_pam_faillock_dir # promote to variable set_fact: var_accounts_passwords_pam_faillock_dir: !!str tags: - always - name: Lock Accounts Must Persist - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Ensure the pam_faillock.so dir parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*dir\s*= line: dir = {{ var_accounts_passwords_pam_faillock_dir }} state: present when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Ensure the pam_faillock.so dir parameter not in PAM files block: - name: Lock Accounts Must Persist - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Lock Accounts Must Persist - Check the proper remediation for the system block: - name: Lock Accounts Must Persist - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Lock Accounts Must Persist - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Lock Accounts Must Persist - Ensure authselect custom profile is used if authselect is present block: - name: Lock Accounts Must Persist - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Lock Accounts Must Persist - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Lock Accounts Must Persist - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Lock Accounts Must Persist - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Lock Accounts Must Persist - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Lock Accounts Must Persist - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts Must Persist - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts Must Persist - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts Must Persist - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts Must Persist - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts Must Persist - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts Must Persist - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Lock Accounts Must Persist - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Lock Accounts Must Persist - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Lock Accounts Must Persist - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Lock Accounts Must Persist - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Lock Accounts Must Persist - Ensure the "dir" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bdir\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Lock Accounts Must Persist - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Lock Accounts Must Persist - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Lock Accounts Must Persist - Check the proper remediation for the system block: - name: Lock Accounts Must Persist - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Lock Accounts Must Persist - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Lock Accounts Must Persist - Ensure authselect custom profile is used if authselect is present block: - name: Lock Accounts Must Persist - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Lock Accounts Must Persist - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Lock Accounts Must Persist - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Lock Accounts Must Persist - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Lock Accounts Must Persist - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Lock Accounts Must Persist - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts Must Persist - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Lock Accounts Must Persist - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts Must Persist - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Lock Accounts Must Persist - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts Must Persist - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Lock Accounts Must Persist - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Lock Accounts Must Persist - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Lock Accounts Must Persist - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Lock Accounts Must Persist - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Lock Accounts Must Persist - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Lock Accounts Must Persist - Ensure the "dir" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bdir\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Lock Accounts Must Persist - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Ensure the pam_faillock.so dir parameter in PAM files block: - name: Lock Accounts Must Persist - Check if pam_faillock.so dir parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*dir state: absent check_mode: true changed_when: false register: result_pam_faillock_dir_parameter_is_present - name: Lock Accounts Must Persist - Ensure the inclusion of pam_faillock.so preauth dir parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 dir={{ var_accounts_passwords_pam_faillock_dir }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_dir_parameter_is_present.found == 0 - name: Lock Accounts Must Persist - Ensure the inclusion of pam_faillock.so authfail dir parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*) line: \1required\3 dir={{ var_accounts_passwords_pam_faillock_dir }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_dir_parameter_is_present.found == 0 - name: Lock Accounts Must Persist - Ensure the desired value for pam_faillock.so preauth dir parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(dir)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_dir }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_dir_parameter_is_present.found > 0 - name: Lock Accounts Must Persist - Ensure the desired value for pam_faillock.so authfail dir parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(dir)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_dir }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_dir_parameter_is_present.found > 0 when: - '"pam" in ansible_facts.packages' - not result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Ensure necessary SELinux packages are installed ansible.builtin.package: name: '{{ item }}' state: present with_items: - python3-libselinux - python3-policycoreutils - policycoreutils-python-utils when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Create the tally directory if it does not exist ansible.builtin.file: path: '{{ var_accounts_passwords_pam_faillock_dir }}' state: directory setype: faillog_t when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Ensure SELinux file context is permanently set ansible.builtin.command: cmd: semanage fcontext -a -t faillog_t "{{ var_accounts_passwords_pam_faillock_dir }}(/.*)?" register: result_accounts_passwords_pam_faillock_dir_semanage failed_when: false changed_when: - result_accounts_passwords_pam_faillock_dir_semanage.rc == 0 when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Lock Accounts Must Persist - Ensure SELinux file context is applied ansible.builtin.command: cmd: restorecon -R "{{ var_accounts_passwords_pam_faillock_dir }}" register: result_accounts_passwords_pam_faillock_dir_restorecon when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AC-7(b) - NIST-800-53-AC-7.1(ii) - accounts_passwords_pam_faillock_dir - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Enforce pam_faillock for Local Accounts Only The pam_faillock module's local_users_only parameter controls requirements for enforcing failed lockout attempts only for local user accounts and ignoring centralized user account management failed attempt configurations. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file. Using this rule bypasses pam_faillock's functionality and should be used in cases where centralized management such as LDAP or Active Directory is in use. AC-2(1) SRG-OS-000001-GPOS-00001 The operating system must provide automated mechanisms for supporting account management functions. Enterprise environments make application account management challenging and complex. A manual process for account management functions adds the risk of a potential oversight or other error. Locking out remote accounts may cause unintentional DoS. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*local_users_only" line="local_users_only" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\blocal_users_only\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\blocal_users_only\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*local_users_only' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ local_users_only/' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ local_users_only/' "$pam_file" fi done fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Enforce pam_faillock for Local Accounts Only - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Enforce pam_faillock for Local Accounts Only - Remediation where authselect tool is present block: - name: Enforce pam_faillock for Local Accounts Only - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Enforce pam_faillock for Local Accounts Only - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Enforce pam_faillock for Local Accounts Only - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Enforce pam_faillock for Local Accounts Only - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Enforce pam_faillock for Local Accounts Only - Remediation where authselect tool is not present block: - name: Enforce pam_faillock for Local Accounts Only - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Enforce pam_faillock for Local Accounts Only - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Enforce pam_faillock for Local Accounts Only - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Enforce pam_faillock for Local Accounts Only - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: - '"pam" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Enforce pam_faillock for Local Accounts Only - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Enforce pam_faillock for Local Accounts Only - Ensure the pam_faillock.so local_users_only parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*local_users_only line: local_users_only state: present when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Enforce pam_faillock for Local Accounts Only - Ensure the pam_faillock.so local_users_only parameter not in PAM files block: - name: Enforce pam_faillock for Local Accounts Only - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Enforce pam_faillock for Local Accounts Only - Check the proper remediation for the system block: - name: Enforce pam_faillock for Local Accounts Only - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Enforce pam_faillock for Local Accounts Only - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect custom profile is used if authselect is present block: - name: Enforce pam_faillock for Local Accounts Only - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Enforce pam_faillock for Local Accounts Only - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Enforce pam_faillock for Local Accounts Only - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Enforce pam_faillock for Local Accounts Only - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Enforce pam_faillock for Local Accounts Only - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Enforce pam_faillock for Local Accounts Only - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Enforce pam_faillock for Local Accounts Only - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Enforce pam_faillock for Local Accounts Only - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Enforce pam_faillock for Local Accounts Only - Ensure the "local_users_only" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\blocal_users_only\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Enforce pam_faillock for Local Accounts Only - Check the proper remediation for the system block: - name: Enforce pam_faillock for Local Accounts Only - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Enforce pam_faillock for Local Accounts Only - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect custom profile is used if authselect is present block: - name: Enforce pam_faillock for Local Accounts Only - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Enforce pam_faillock for Local Accounts Only - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Enforce pam_faillock for Local Accounts Only - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Enforce pam_faillock for Local Accounts Only - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Enforce pam_faillock for Local Accounts Only - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Enforce pam_faillock for Local Accounts Only - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Enforce pam_faillock for Local Accounts Only - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Enforce pam_faillock for Local Accounts Only - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Enforce pam_faillock for Local Accounts Only - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Enforce pam_faillock for Local Accounts Only - Ensure the "local_users_only" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\blocal_users_only\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Enforce pam_faillock for Local Accounts Only - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Enforce pam_faillock for Local Accounts Only - Ensure the pam_faillock.so local_users_only parameter in PAM files block: - name: Enforce pam_faillock for Local Accounts Only - Check if pam_faillock.so local_users_only parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*local_users_only state: absent check_mode: true changed_when: false register: result_pam_faillock_local_users_only_parameter_is_present - name: Enforce pam_faillock for Local Accounts Only - Ensure the inclusion of pam_faillock.so preauth local_users_only parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 local_users_only state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_local_users_only_parameter_is_present.found == 0 - name: Enforce pam_faillock for Local Accounts Only - Ensure the inclusion of pam_faillock.so authfail local_users_only parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*) line: \1required\3 local_users_only state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_local_users_only_parameter_is_present.found == 0 when: - '"pam" in ansible_facts.packages' - not result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-2(1) - accounts_passwords_pam_faillock_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Interval For Counting Failed Password Attempts Utilizing pam_faillock.so, the fail_interval directive configures the system to lock out an account after a number of incorrect login attempts within a specified time period. Ensure that the file /etc/security/faillock.conf contains the following entry: fail_interval = <interval-in-seconds> where interval-in-seconds is or greater. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) AC-7(a) PR.AC-7 FIA_AFL.1 SRG-OS-000329-GPOS-00128 SRG-OS-000021-GPOS-00005 R31 By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_accounts_passwords_pam_faillock_fail_interval='' if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*fail_interval\s*=" line="fail_interval = $var_accounts_passwords_pam_faillock_fail_interval" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF else sed -i --follow-symlinks 's|^\s*\(fail_interval\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_fail_interval"'|g' $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\bfail_interval\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\bfail_interval\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*fail_interval' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ fail_interval='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ fail_interval='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file" else sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*\)\('"fail_interval"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'\3/' "$pam_file" sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"fail_interval"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'\3/' "$pam_file" fi done fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Interval For Counting Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Interval For Counting Failed Password Attempts - Remediation where authselect tool is present block: - name: Set Interval For Counting Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set Interval For Counting Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set Interval For Counting Failed Password Attempts - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Set Interval For Counting Failed Password Attempts - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Interval For Counting Failed Password Attempts - Remediation where authselect tool is not present block: - name: Set Interval For Counting Failed Password Attempts - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Set Interval For Counting Failed Password Attempts - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Set Interval For Counting Failed Password Attempts - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Set Interval For Counting Failed Password Attempts - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: - '"pam" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_passwords_pam_faillock_fail_interval # promote to variable set_fact: var_accounts_passwords_pam_faillock_fail_interval: !!str tags: - always - name: Set Interval For Counting Failed Password Attempts - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so fail_interval parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*fail_interval\s*= line: fail_interval = {{ var_accounts_passwords_pam_faillock_fail_interval }} state: present when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so fail_interval parameter not in PAM files block: - name: Set Interval For Counting Failed Password Attempts - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Set Interval For Counting Failed Password Attempts - Check the proper remediation for the system block: - name: Set Interval For Counting Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Set Interval For Counting Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set Interval For Counting Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Set Interval For Counting Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set Interval For Counting Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set Interval For Counting Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set Interval For Counting Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set Interval For Counting Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set Interval For Counting Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Interval For Counting Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Interval For Counting Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Interval For Counting Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Interval For Counting Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set Interval For Counting Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Set Interval For Counting Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Set Interval For Counting Failed Password Attempts - Ensure the "fail_interval" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bfail_interval\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Set Interval For Counting Failed Password Attempts - Check the proper remediation for the system block: - name: Set Interval For Counting Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Set Interval For Counting Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set Interval For Counting Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Set Interval For Counting Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set Interval For Counting Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set Interval For Counting Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set Interval For Counting Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set Interval For Counting Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set Interval For Counting Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Interval For Counting Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Interval For Counting Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Interval For Counting Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Interval For Counting Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set Interval For Counting Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Set Interval For Counting Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Set Interval For Counting Failed Password Attempts - Ensure the "fail_interval" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bfail_interval\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so fail_interval parameter in PAM files block: - name: Set Interval For Counting Failed Password Attempts - Check if pam_faillock.so fail_interval parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*fail_interval state: absent check_mode: true changed_when: false register: result_pam_faillock_fail_interval_parameter_is_present - name: Set Interval For Counting Failed Password Attempts - Ensure the inclusion of pam_faillock.so preauth fail_interval parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 fail_interval={{ var_accounts_passwords_pam_faillock_fail_interval }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_fail_interval_parameter_is_present.found == 0 - name: Set Interval For Counting Failed Password Attempts - Ensure the inclusion of pam_faillock.so authfail fail_interval parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*) line: \1required\3 fail_interval={{ var_accounts_passwords_pam_faillock_fail_interval }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_fail_interval_parameter_is_present.found == 0 - name: Set Interval For Counting Failed Password Attempts - Ensure the desired value for pam_faillock.so preauth fail_interval parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(fail_interval)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_fail_interval }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_fail_interval_parameter_is_present.found > 0 - name: Set Interval For Counting Failed Password Attempts - Ensure the desired value for pam_faillock.so authfail fail_interval parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(fail_interval)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_fail_interval }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_fail_interval_parameter_is_present.found > 0 when: - '"pam" in ansible_facts.packages' - not result_faillock_conf_check.stat.exists tags: - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - accounts_passwords_pam_faillock_interval - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Lockout Time for Failed Password Attempts This rule configures the system to lock out accounts during a specified time period after a number of incorrect login attempts using pam_faillock.so. Ensure that the file /etc/security/faillock.conf contains the following entry: unlock_time=<interval-in-seconds> where interval-in-seconds is or greater. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid any errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version. If unlock_time is set to 0, manual intervention by an administrator is required to unlock a user. This should be done using the faillock tool. If the system supports the new /etc/security/faillock.conf file but the pam_faillock.so parameters are defined directly in /etc/pam.d/system-auth and /etc/pam.d/password-auth, the remediation will migrate the unlock_time parameter to /etc/security/faillock.conf to ensure compatibility with authselect tool. The parameters deny and fail_interval, if used, also have to be migrated by their respective remediation. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file. 1 12 15 16 5.5.3 DSS05.04 DSS05.10 DSS06.10 3.1.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) AC-7(b) PR.AC-7 FIA_AFL.1 Req-8.1.7 SRG-OS-000329-GPOS-00128 SRG-OS-000021-GPOS-00005 R31 5.3.3.1.2 8.3.4 8.3 By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_accounts_passwords_pam_faillock_unlock_time='' if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-faillock authselect apply-changes -b else AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth required pam_faillock.so preauth silent' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth required pam_faillock.so authfail' "$pam_file" sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account required pam_faillock.so' "$pam_file" fi sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required \3/g' "$pam_file" done fi AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth") SKIP_FAILLOCK_CHECK=false FAILLOCK_CONF="/etc/security/faillock.conf" if [ -f $FAILLOCK_CONF ] || [ "$SKIP_FAILLOCK_CHECK" = "true" ]; then regex="^\s*unlock_time\s*=" line="unlock_time = $var_accounts_passwords_pam_faillock_unlock_time" if ! grep -q $regex $FAILLOCK_CONF; then echo $line >> $FAILLOCK_CONF else sed -i --follow-symlinks 's|^\s*\(unlock_time\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_unlock_time"'|g' $FAILLOCK_CONF fi for pam_file in "${AUTH_FILES[@]}" do if [ -e "$pam_file" ] ; then PAM_FILE_PATH="$pam_file" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$pam_file") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*auth\s.*\bpam_faillock.so\s.*\bunlock_time\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*auth.*pam_faillock.so.*)\bunlock_time\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$pam_file was not found" >&2 fi done else for pam_file in "${AUTH_FILES[@]}" do if ! grep -qE '^\s*auth.*pam_faillock\.so\s+(preauth|authfail).*unlock_time' "$pam_file"; then sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*/ s/$/ unlock_time='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file" sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ unlock_time='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file" else sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*\)\('"unlock_time"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'\3/' "$pam_file" sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"unlock_time"'=\)\S\+\b\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'\3/' "$pam_file" fi done fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Lockout Time for Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Lockout Time for Failed Password Attempts - Remediation where authselect tool is present block: - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set Lockout Time for Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set Lockout Time for Failed Password Attempts - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Set Lockout Time for Failed Password Attempts - Ensure "with-faillock" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-faillock register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-faillock") - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"pam" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Lockout Time for Failed Password Attempts - Remediation where authselect tool is not present block: - name: Set Lockout Time for Failed Password Attempts - Check if pam_faillock.so is already enabled ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail) state: absent check_mode: true changed_when: false register: result_pam_faillock_is_enabled - name: Set Lockout Time for Failed Password Attempts - Enable pam_faillock.so preauth editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so preauth insertbefore: ^auth.*sufficient.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Set Lockout Time for Failed Password Attempts - Enable pam_faillock.so authfail editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: auth required pam_faillock.so authfail insertbefore: ^auth.*required.*pam_deny\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 - name: Set Lockout Time for Failed Password Attempts - Enable pam_faillock.so account section editing PAM files ansible.builtin.lineinfile: path: '{{ item }}' line: account required pam_faillock.so insertbefore: ^account.*required.*pam_unix\.so.* state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_is_enabled.found == 0 when: - '"pam" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_passwords_pam_faillock_unlock_time # promote to variable set_fact: var_accounts_passwords_pam_faillock_unlock_time: !!str tags: - always - name: Set Lockout Time for Failed Password Attempts - Check the presence of /etc/security/faillock.conf file ansible.builtin.stat: path: /etc/security/faillock.conf register: result_faillock_conf_check when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so unlock_time parameter in /etc/security/faillock.conf ansible.builtin.lineinfile: path: /etc/security/faillock.conf regexp: ^\s*unlock_time\s*= line: unlock_time = {{ var_accounts_passwords_pam_faillock_unlock_time }} state: present when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so unlock_time parameter not in PAM files block: - name: Set Lockout Time for Failed Password Attempts - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present - name: Set Lockout Time for Failed Password Attempts - Check the proper remediation for the system block: - name: Set Lockout Time for Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Set Lockout Time for Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set Lockout Time for Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set Lockout Time for Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set Lockout Time for Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set Lockout Time for Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set Lockout Time for Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set Lockout Time for Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Lockout Time for Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Lockout Time for Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Lockout Time for Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Lockout Time for Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set Lockout Time for Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Set Lockout Time for Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Set Lockout Time for Failed Password Attempts - Ensure the "unlock_time" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bunlock_time\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present - name: Set Lockout Time for Failed Password Attempts - Check the proper remediation for the system block: - name: Set Lockout Time for Failed Password Attempts - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Set Lockout Time for Failed Password Attempts - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set Lockout Time for Failed Password Attempts - Ensure authselect custom profile is used if authselect is present block: - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set Lockout Time for Failed Password Attempts - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set Lockout Time for Failed Password Attempts - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set Lockout Time for Failed Password Attempts - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set Lockout Time for Failed Password Attempts - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set Lockout Time for Failed Password Attempts - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Lockout Time for Failed Password Attempts - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set Lockout Time for Failed Password Attempts - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Lockout Time for Failed Password Attempts - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set Lockout Time for Failed Password Attempts - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set Lockout Time for Failed Password Attempts - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '' - name: Set Lockout Time for Failed Password Attempts - Check if {{ pam_file_path }} file is present ansible.builtin.stat: path: '{{ pam_file_path }}' register: result_pam_file_present - name: Set Lockout Time for Failed Password Attempts - Ensure the "unlock_time" option from "pam_faillock.so" is not present in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (.*auth.*pam_faillock.so.*)\bunlock_time\b=?[0-9a-zA-Z]*(.*) replace: \1\2 register: result_pam_option_removal when: result_pam_file_present.stat.exists - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_option_removal is changed when: - result_pam_file_present.stat.exists when: - '"pam" in ansible_facts.packages' - result_faillock_conf_check.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so unlock_time parameter in PAM files block: - name: Set Lockout Time for Failed Password Attempts - Check if pam_faillock.so unlock_time parameter is already enabled in pam files ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: .*auth.*pam_faillock\.so (preauth|authfail).*unlock_time state: absent check_mode: true changed_when: false register: result_pam_faillock_unlock_time_parameter_is_present - name: Set Lockout Time for Failed Password Attempts - Ensure the inclusion of pam_faillock.so preauth unlock_time parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*) line: \1required\3 unlock_time={{ var_accounts_passwords_pam_faillock_unlock_time }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_unlock_time_parameter_is_present.found == 0 - name: Set Lockout Time for Failed Password Attempts - Ensure the inclusion of pam_faillock.so authfail unlock_time parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*) line: \1required\3 unlock_time={{ var_accounts_passwords_pam_faillock_unlock_time }} state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_unlock_time_parameter_is_present.found == 0 - name: Set Lockout Time for Failed Password Attempts - Ensure the desired value for pam_faillock.so preauth unlock_time parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(unlock_time)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_unlock_time }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_unlock_time_parameter_is_present.found > 0 - name: Set Lockout Time for Failed Password Attempts - Ensure the desired value for pam_faillock.so authfail unlock_time parameter in auth section ansible.builtin.lineinfile: path: '{{ item }}' backrefs: true regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(unlock_time)=[0-9]+(.*) line: \1required\3\4={{ var_accounts_passwords_pam_faillock_unlock_time }}\5 state: present loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - result_pam_faillock_unlock_time_parameter_is_present.found > 0 when: - '"pam" in ansible_facts.packages' - not result_faillock_conf_check.stat.exists tags: - CJIS-5.5.3 - NIST-800-171-3.1.8 - NIST-800-53-AC-7(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.1.7 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.4 - accounts_passwords_pam_faillock_unlock_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Password Quality Requirements The default pam_pwquality PAM module provides strength checking for passwords. It performs a number of checks, such as making sure passwords are not similar to dictionary words, are of at least a certain length, are not the previous password reversed, and are not simply a change of case from the previous password. It can also require passwords to be in certain character classes. The pam_pwquality module is the preferred way of configuring password requirements. The man pages pam_pwquality(8) provide information on the capabilities and configuration of each. Set Password Quality Requirements with pam_pwquality The pam_pwquality PAM module can be configured to meet requirements for a variety of policies. For example, to configure pam_pwquality to require at least one uppercase character, lowercase character, digit, and other (special) character, make sure that pam_pwquality exists in /etc/pam.d/system-auth: password requisite pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type= If no such line exists, add one as the first line of the password section in /etc/pam.d/system-auth. Next, modify the settings in /etc/security/pwquality.conf to match the following: difok = 4 minlen = 14 dcredit = -1 ucredit = -1 lcredit = -1 ocredit = -1 maxrepeat = 3 The arguments can be modified to ensure compliance with your organization's security policy. Discussion of each parameter follows. dcredit Minimum number of digits in password 0 -1 -2 -1 dictcheck Prevent the use of dictionary words for passwords. 1 1 difok Minimum number of characters not present in old password 15 1 2 3 4 5 6 7 8 8 lcredit Minimum number of lower case in password 0 -1 -2 -1 maxclassrepeat Maximum Number of Consecutive Repeating Characters in a Password From the Same Character Class 1 2 3 4 4 maxrepeat Maximum Number of Consecutive Repeating Characters in a Password 1 2 3 3 minclass Minimum number of categories of characters that must exist in a password 1 2 3 4 3 minlen Minimum number of characters in password 10 12 14 15 17 18 20 6 7 8 15 ocredit Minimum number of other (special characters) in password 0 -1 -2 -1 retry Number of retry attempts before erroring out 1 2 3 4 5 3 ucredit Minimum number of upper case in password 0 -1 -2 -1 Ensure PAM Enforces Password Requirements - Minimum Digit Characters The pam_pwquality module's dcredit parameter controls requirements for usage of digits in a password. When set to a negative number, any password will be required to contain that many digits. When set to a positive number, pam_pwquality will grant +1 additional length credit for each digit. Modify the dcredit setting in /etc/security/pwquality.conf to require the use of a digit in passwords. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 FMT_SMF_EXT.1 Req-8.2.3 SRG-OS-000071-GPOS-00039 R31 8.3.6 8.3 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring digits makes password guessing attacks more difficult by ensuring a larger search space. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_dcredit='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^dcredit") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_dcredit" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^dcredit\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^dcredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.6 - accounts_password_pam_dcredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_dcredit # promote to variable set_fact: var_password_pam_dcredit: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure PAM variable dcredit is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*dcredit line: dcredit = {{ var_password_pam_dcredit }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.6 - accounts_password_pam_dcredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary Words The pam_pwquality module's dictcheck check if passwords contains dictionary words. When dictcheck is set to 1 passwords will be checked for dictionary words. IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) SRG-OS-000480-GPOS-00225 SRG-OS-000072-GPOS-00040 5.3.3.2.6 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Passwords with dictionary words may be more vulnerable to password-guessing attacks. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_dictcheck='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^dictcheck") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_dictcheck" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^dictcheck\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^dictcheck\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_dictcheck - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_dictcheck # promote to variable set_fact: var_password_pam_dictcheck: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary Words - Ensure PAM variable dictcheck is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*dictcheck line: dictcheck = {{ var_password_pam_dictcheck }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_dictcheck - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Minimum Different Characters The pam_pwquality module's difok parameter sets the number of characters in a password that must not be present in and old password during a password change. Modify the difok setting in /etc/security/pwquality.conf to equal to require differing characters when changing passwords. 1 12 15 16 5 5.6.2.1.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(b) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000072-GPOS-00040 5.3.3.2.1 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute–force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring a minimum number of different characters during password changes ensures that newly changed passwords should not resemble previously compromised ones. Note that passwords which are changed on compromised systems will still be compromised, however. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_difok='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^difok") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_difok" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^difok\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^difok\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1.1 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(b) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_difok - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_difok # promote to variable set_fact: var_password_pam_difok: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters - Ensure PAM variable difok is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*difok line: difok = {{ var_password_pam_difok }} when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(b) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_difok - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Enforce for Local Accounts Only The pam_pwquality module's local_users_only parameter controls requirements for enforcing password complexity by pam_pwquality only for local user accounts and ignoring centralized user account management password complexity configurations. Enable the local_users_only setting in /etc/security/pwquality.conf to require password complexity enforcement for only local user accounts. Using this rule bypasses pam_faillock's functionality and should be used in cases where centralized management such as LDAP or Active Directory is in use. AC-2(1) SRG-OS-000001-GPOS-00001 The operating system must provide automated mechanisms for supporting account management functions. Enterprise environments make application account management challenging and complex. A manual process for account management functions adds the risk of a potential oversight or other error. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then if [ -e "/etc/security/pwquality.conf" ] ; then LC_ALL=C sed -i "/^\s*local_users_only/Id" "/etc/security/pwquality.conf" else touch "/etc/security/pwquality.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/security/pwquality.conf" cp "/etc/security/pwquality.conf" "/etc/security/pwquality.conf.bak" # Insert at the end of the file printf '%s\n' "local_users_only" >> "/etc/security/pwquality.conf" # Clean up after ourselves. rm "/etc/security/pwquality.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(1) - accounts_password_pam_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure PAM Enforces Password Requirements - Enforce for Local Accounts Only ansible.builtin.lineinfile: path: /etc/security/pwquality.conf create: true regexp: '' line: local_users_only state: present when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-AC-2(1) - accounts_password_pam_enforce_local - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Enforce for root User The pam_pwquality module's enforce_for_root parameter controls requirements for enforcing password complexity for the root user. Enable the enforce_for_root setting in /etc/security/pwquality.conf to require the root user to use complex passwords. IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) SRG-OS-000072-GPOS-00040 SRG-OS-000071-GPOS-00039 SRG-OS-000070-GPOS-00038 SRG-OS-000266-GPOS-00101 SRG-OS-000078-GPOS-00046 SRG-OS-000480-GPOS-00225 SRG-OS-000069-GPOS-00037 5.3.3.2.7 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then if [ -e "/etc/security/pwquality.conf" ] ; then LC_ALL=C sed -i "/^\s*enforce_for_root/Id" "/etc/security/pwquality.conf" else touch "/etc/security/pwquality.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/security/pwquality.conf" cp "/etc/security/pwquality.conf" "/etc/security/pwquality.conf.bak" # Insert at the end of the file printf '%s\n' "enforce_for_root" >> "/etc/security/pwquality.conf" # Clean up after ourselves. rm "/etc/security/pwquality.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_enforce_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure PAM Enforces Password Requirements - Enforce for root User ansible.builtin.lineinfile: path: /etc/security/pwquality.conf create: true regexp: '' line: enforce_for_root state: present when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_enforce_root - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters The pam_pwquality module's lcredit parameter controls requirements for usage of lowercase letters in a password. When set to a negative number, any password will be required to contain that many lowercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each lowercase character. Modify the lcredit setting in /etc/security/pwquality.conf to require the use of a lowercase character in passwords. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 FMT_SMF_EXT.1 Req-8.2.3 SRG-OS-000070-GPOS-00038 R31 8.3.6 8.3 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possble combinations that need to be tested before the password is compromised. Requiring a minimum number of lowercase characters makes password guessing attacks more difficult by ensuring a larger search space. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_lcredit='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^lcredit") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_lcredit" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^lcredit\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^lcredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.6 - accounts_password_pam_lcredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_lcredit # promote to variable set_fact: var_password_pam_lcredit: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters - Ensure PAM variable lcredit is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*lcredit line: lcredit = {{ var_password_pam_lcredit }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.6 - accounts_password_pam_lcredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating Characters from Same Character Class The pam_pwquality module's maxclassrepeat parameter controls requirements for consecutive repeating characters from the same character class. When set to a positive number, it will reject passwords which contain more than that number of consecutive characters from the same character class. Modify the maxclassrepeat setting in /etc/security/pwquality.conf to equal to prevent a run of ( + 1) or more identical characters. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000072-GPOS-00040 SRG-OS-000730-GPOS-00190 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex a password, the greater the number of possible combinations that need to be tested before the password is compromised. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_maxclassrepeat='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^maxclassrepeat") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_maxclassrepeat" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^maxclassrepeat\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^maxclassrepeat\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_maxclassrepeat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_maxclassrepeat # promote to variable set_fact: var_password_pam_maxclassrepeat: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating Characters from Same Character Class - Ensure PAM variable maxclassrepeat is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*maxclassrepeat line: maxclassrepeat = {{ var_password_pam_maxclassrepeat }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_maxclassrepeat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Password Maximum Consecutive Repeating Characters The pam_pwquality module's maxrepeat parameter controls requirements for consecutive repeating characters. When set to a positive number, it will reject passwords which contain more than that number of consecutive characters. Modify the maxrepeat setting in /etc/security/pwquality.conf to equal to prevent a run of ( + 1) or more identical characters. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000072-GPOS-00040 5.3.3.2.4 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Passwords with excessive repeating characters may be more vulnerable to password-guessing attacks. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_maxrepeat='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^maxrepeat") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_maxrepeat" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^maxrepeat\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^maxrepeat\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_maxrepeat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_maxrepeat # promote to variable set_fact: var_password_pam_maxrepeat: !!str tags: - always - name: Set Password Maximum Consecutive Repeating Characters - Ensure PAM variable maxrepeat is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*maxrepeat line: maxrepeat = {{ var_password_pam_maxrepeat }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_maxrepeat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Minimum Different Categories The pam_pwquality module's minclass parameter controls requirements for usage of different character classes, or types, of character that must exist in a password before it is considered valid. For example, setting this value to three (3) requires that any password must have characters from at least three different categories in order to be approved. The default value is zero (0), meaning there are no required classes. There are four categories available: * Upper-case characters * Lower-case characters * Digits * Special characters (for example, punctuation) Modify the minclass setting in /etc/security/pwquality.conf entry to require differing categories of characters when changing passwords. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000072-GPOS-00040 R68 5.3.3.2.3 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring a minimum number of character categories makes password guessing attacks more difficult by ensuring a larger search space. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_minclass='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^minclass") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_minclass" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^minclass\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^minclass\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_minclass - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_minclass # promote to variable set_fact: var_password_pam_minclass: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories - Ensure PAM variable minclass is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*minclass line: minclass = {{ var_password_pam_minclass }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_minclass - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Minimum Length The pam_pwquality module's minlen parameter controls requirements for minimum characters required in a password. Add minlen= after pam_pwquality to set minimum password length requirements. 1 12 15 16 5 5.6.2.1.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 FMT_SMF_EXT.1 Req-8.2.3 SRG-OS-000078-GPOS-00046 R31 R68 5.3.3.2.2 8.3.6 8.3 The shorter the password, the lower the number of possible combinations that need to be tested before the password is compromised. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password length is one factor of several that helps to determine strength and how long it takes to crack a password. Use of more characters in a password helps to exponentially increase the time and/or resources required to compromise the password. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_minlen='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^minlen") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_minlen" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^minlen\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^minlen\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1.1 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.6 - accounts_password_pam_minlen - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_minlen # promote to variable set_fact: var_password_pam_minlen: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure PAM variable minlen is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*minlen line: minlen = {{ var_password_pam_minlen }} when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.6 - accounts_password_pam_minlen - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM Enforces Password Requirements - Minimum Special Characters The pam_pwquality module's ocredit= parameter controls requirements for usage of special (or "other") characters in a password. When set to a negative number, any password will be required to contain that many special characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each special character. Modify the ocredit setting in /etc/security/pwquality.conf to equal to require use of a special character in passwords. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 FMT_SMF_EXT.1 SRG-OS-000266-GPOS-00101 R31 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring a minimum number of special characters makes password guessing attacks more difficult by ensuring a larger search space. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_ocredit='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ocredit") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_ocredit" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^ocredit\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^ocredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_ocredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_ocredit # promote to variable set_fact: var_password_pam_ocredit: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Ensure PAM variable ocredit is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*ocredit line: ocredit = {{ var_password_pam_ocredit }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - accounts_password_pam_ocredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure PAM password complexity module is enabled in password-auth To enable PAM password complexity in password-auth file: Edit the password section in /etc/pam.d/password-auth to show password requisite pam_pwquality.so. SRG-OS-000069-GPOS-00037 SRG-OS-000070-GPOS-00038 SRG-OS-000480-GPOS-00227 Enabling PAM password complexity permits to enforce strong passwords and consequently makes the system less prone to dictionary attacks. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then if [ -e "/etc/pam.d/password-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/password-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+requisite\s+pam_pwquality.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwquality.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwquality.so.*)/\1requisite \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^account.*required.*pam_permit\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password requisite pam_pwquality.so" "$PAM_FILE_PATH" else echo "password requisite pam_pwquality.so" >> "$PAM_FILE_PATH" fi fi fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/password-auth was not found" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - accounts_password_pam_pwquality_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM password complexity module is enabled in password-auth - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present when: '"libpwquality" in ansible_facts.packages' tags: - accounts_password_pam_pwquality_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM password complexity module is enabled in password-auth - Check the proper remediation for the system block: - name: Ensure PAM password complexity module is enabled in password-auth - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Ensure PAM password complexity module is enabled in password-auth - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Ensure PAM password complexity module is enabled in password-auth - Ensure authselect custom profile is used if authselect is present block: - name: Ensure PAM password complexity module is enabled in password-auth - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Ensure PAM password complexity module is enabled in password-auth - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Ensure PAM password complexity module is enabled in password-auth - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Ensure PAM password complexity module is enabled in password-auth - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Ensure PAM password complexity module is enabled in password-auth - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Ensure PAM password complexity module is enabled in password-auth - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Ensure PAM password complexity module is enabled in password-auth - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Ensure PAM password complexity module is enabled in password-auth - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Ensure PAM password complexity module is enabled in password-auth - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Ensure PAM password complexity module is enabled in password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Ensure PAM password complexity module is enabled in password-auth - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Ensure PAM password complexity module is enabled in password-auth - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Ensure PAM password complexity module is enabled in password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Ensure PAM password complexity module is enabled in password-auth - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Ensure PAM password complexity module is enabled in password-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: requisite - name: Ensure PAM password complexity module is enabled in password-auth - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwquality.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Ensure PAM password complexity module is enabled in password-auth - Include or update the PAM module line in {{ pam_file_path }} block: - name: Ensure PAM password complexity module is enabled in password-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwquality.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Ensure PAM password complexity module is enabled in password-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwquality.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Ensure PAM password complexity module is enabled in password-auth - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' insertafter: ^account.*required.*pam_permit\.so line: password {{ pam_module_control }} pam_pwquality.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Ensure PAM password complexity module is enabled in password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Ensure PAM password complexity module is enabled in password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - |- (result_pam_accounts_password_pam_pwquality_password_auth_add is defined and result_pam_accounts_password_pam_pwquality_password_auth_add.changed) or (result_pam_accounts_password_pam_pwquality_password_auth_edit is defined and result_pam_accounts_password_pam_pwquality_password_auth_edit.changed) when: - '"libpwquality" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - accounts_password_pam_pwquality_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed Ensure PAM password complexity module is enabled in system-auth To enable PAM password complexity in system-auth file: Edit the password section in /etc/pam.d/system-auth to show password requisite pam_pwquality.so. SRG-OS-000480-GPOS-00227 Enabling PAM password complexity permits to enforce strong passwords and consequently makes the system less prone to dictionary attacks. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then if [ -e "/etc/pam.d/system-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/system-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+requisite\s+pam_pwquality.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwquality.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwquality.so.*)/\1requisite \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^account.*required.*pam_permit\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password requisite pam_pwquality.so" "$PAM_FILE_PATH" else echo "password requisite pam_pwquality.so" >> "$PAM_FILE_PATH" fi fi fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/system-auth was not found" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - accounts_password_pam_pwquality_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM password complexity module is enabled in system-auth - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present when: '"libpwquality" in ansible_facts.packages' tags: - accounts_password_pam_pwquality_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM password complexity module is enabled in system-auth - Check the proper remediation for the system block: - name: Ensure PAM password complexity module is enabled in system-auth - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Ensure PAM password complexity module is enabled in system-auth - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Ensure PAM password complexity module is enabled in system-auth - Ensure authselect custom profile is used if authselect is present block: - name: Ensure PAM password complexity module is enabled in system-auth - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Ensure PAM password complexity module is enabled in system-auth - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Ensure PAM password complexity module is enabled in system-auth - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Ensure PAM password complexity module is enabled in system-auth - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Ensure PAM password complexity module is enabled in system-auth - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Ensure PAM password complexity module is enabled in system-auth - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Ensure PAM password complexity module is enabled in system-auth - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Ensure PAM password complexity module is enabled in system-auth - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Ensure PAM password complexity module is enabled in system-auth - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Ensure PAM password complexity module is enabled in system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Ensure PAM password complexity module is enabled in system-auth - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Ensure PAM password complexity module is enabled in system-auth - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Ensure PAM password complexity module is enabled in system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Ensure PAM password complexity module is enabled in system-auth - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Ensure PAM password complexity module is enabled in system-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: requisite - name: Ensure PAM password complexity module is enabled in system-auth - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwquality.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Ensure PAM password complexity module is enabled in system-auth - Include or update the PAM module line in {{ pam_file_path }} block: - name: Ensure PAM password complexity module is enabled in system-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_pwquality.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Ensure PAM password complexity module is enabled in system-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_pwquality.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Ensure PAM password complexity module is enabled in system-auth - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' insertafter: ^account.*required.*pam_permit\.so line: password {{ pam_module_control }} pam_pwquality.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Ensure PAM password complexity module is enabled in system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Ensure PAM password complexity module is enabled in system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - |- (result_pam_accounts_password_pam_pwquality_system_auth_add is defined and result_pam_accounts_password_pam_pwquality_system_auth_add.changed) or (result_pam_accounts_password_pam_pwquality_system_auth_edit is defined and result_pam_accounts_password_pam_pwquality_system_auth_edit.changed) when: - '"libpwquality" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - accounts_password_pam_pwquality_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session To configure the number of retry prompts that are permitted per-session: Edit the pam_pwquality.so statement in /etc/pam.d/system-auth to show retry= , or a lower value if site policy is more restrictive. The profile requirement is a maximum of retry= prompts per session. 1 11 12 15 16 3 5 9 5.5.3 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) AC-7(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 PR.IP-1 SRG-OS-000069-GPOS-00037 SRG-OS-000480-GPOS-00227 R68 Setting the password retry prompts that are permitted on a per-session basis to a low value requires some software, such as SSH, to re-connect. This can slow down and draw additional attention to some types of password-guessing attacks. Note that this is different from account lockout, which is provided by the pam_faillock module. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_retry='' if [ -e "/etc/pam.d/system-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/system-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+requisite\s+pam_pwquality.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwquality.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwquality.so.*)/\1requisite \2/" "$PAM_FILE_PATH" else LAST_MATCH_LINE=$(grep -nP "^\s*account" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1) if [ ! -z $LAST_MATCH_LINE ]; then sed -i --follow-symlinks $LAST_MATCH_LINE" a password requisite pam_pwquality.so" "$PAM_FILE_PATH" else echo "password requisite pam_pwquality.so" >> "$PAM_FILE_PATH" fi fi fi # Check the option if ! grep -qP "^\s*password\s+requisite\s+pam_pwquality.so\s*.*\sretry\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+requisite\s+pam_pwquality.so.*/ s/$/ retry=$var_password_pam_retry/" "$PAM_FILE_PATH" else sed -i -E --follow-symlinks "s/(\s*password\s+requisite\s+pam_pwquality.so\s+.*)(retry=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_retry \3/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/system-auth was not found" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_password_pam_retry # promote to variable set_fact: var_password_pam_retry: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: requisite when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Check if expected PAM module line is present in /etc/pam.d/system-auth ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwquality.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Include or update the PAM module line in /etc/pam.d/system-auth block: - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Check if required PAM module line is present in /etc/pam.d/system-auth with different control ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: ^\s*password\s+.*\s+pam_pwquality.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Ensure the correct control for the required PAM module line in /etc/pam.d/system-auth ansible.builtin.replace: dest: /etc/pam.d/system-auth regexp: ^(\s*password\s+).*(\bpam_pwquality.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Ensure the required PAM module line is included in /etc/pam.d/system-auth ansible.builtin.lineinfile: dest: /etc/pam.d/system-auth insertafter: ^\s*account line: password {{ pam_module_control }} pam_pwquality.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - '"libpwquality" in ansible_facts.packages' - result_pam_line_present.found is defined - result_pam_line_present.found == 0 tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: requisite when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Check if the required PAM module option is present in /etc/pam.d/system-auth ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwquality.so\s*.*\sretry\b state: absent check_mode: true changed_when: false register: result_pam_module_accounts_password_pam_retry_option_present when: '"libpwquality" in ansible_facts.packages' tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Ensure the "retry" PAM option for "pam_pwquality.so" is included in /etc/pam.d/system-auth ansible.builtin.lineinfile: path: /etc/pam.d/system-auth backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwquality.so.*) line: \1 retry={{ var_password_pam_retry }} state: present register: result_pam_accounts_password_pam_retry_add when: - '"libpwquality" in ansible_facts.packages' - result_pam_module_accounts_password_pam_retry_option_present.found is defined - result_pam_module_accounts_password_pam_retry_option_present.found == 0 tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session - Ensure the required value for "retry" PAM option from "pam_pwquality.so" in /etc/pam.d/system-auth ansible.builtin.lineinfile: path: /etc/pam.d/system-auth backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwquality.so\s+.*)(retry)=[0-9a-zA-Z]*\s*(.*) line: \1\2={{ var_password_pam_retry }} \3 register: result_pam_accounts_password_pam_retry_edit when: - '"libpwquality" in ansible_facts.packages' - result_pam_module_accounts_password_pam_retry_option_present.found > 0 tags: - CJIS-5.5.3 - NIST-800-53-AC-7(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(4) - accounts_password_pam_retry - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters The pam_pwquality module's ucredit= parameter controls requirements for usage of uppercase letters in a password. When set to a negative number, any password will be required to contain that many uppercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each uppercase character. Modify the ucredit setting in /etc/security/pwquality.conf to require the use of an uppercase character in passwords. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(a) CM-6(a) IA-5(4) PR.AC-1 PR.AC-6 PR.AC-7 FMT_SMF_EXT.1 Req-8.2.3 SRG-OS-000069-GPOS-00037 SRG-OS-000070-GPOS-00038 R31 Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. # Remediation is applicable only in certain platforms if rpm --quiet -q libpwquality; then var_password_pam_ucredit='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ucredit") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_ucredit" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^ucredit\\>" "/etc/security/pwquality.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^ucredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf" else if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf" fi printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - accounts_password_pam_ucredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_password_pam_ucredit # promote to variable set_fact: var_password_pam_ucredit: !!str tags: - always - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters - Ensure PAM variable ucredit is set accordingly ansible.builtin.lineinfile: create: true dest: /etc/security/pwquality.conf regexp: ^#?\s*ucredit line: ucredit = {{ var_password_pam_ucredit }} when: '"libpwquality" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(4) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - accounts_password_pam_ucredit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Password Hashing Algorithm The system's default algorithm for storing password hashes in /etc/shadow is SHA-512. This can be configured in several locations. Set Password Hashing Algorithm in /etc/libuser.conf In /etc/libuser.conf, add or correct the following line in its [defaults] section to ensure the system will use the algorithm for password hashing: crypt_style = 1 12 15 16 5 5.6.2.2 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.13.11 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0418 1055 1402 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(c) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.1 SRG-OS-000073-GPOS-00041 5.4.1.4 8.3.2 8.3 Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text. This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the crypt_style configuration option in /etc/libuser.conf ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult. # Remediation is applicable only in certain platforms if rpm --quiet -q libuser; then var_password_hashing_algorithm_pam='' LIBUSER_CONF="/etc/libuser.conf" CRYPT_STYLE_REGEX='[[:space:]]*\[defaults](.*(\n)+)+?[[:space:]]*crypt_style[[:space:]]*' # Try find crypt_style in [defaults] section. If it is here, then change algorithm to sha512. # If it isn't here, then add it to [defaults] section. if grep -qzosP $CRYPT_STYLE_REGEX $LIBUSER_CONF ; then sed -i "s/\(crypt_style[[:space:]]*=[[:space:]]*\).*/\1$var_password_hashing_algorithm_pam/g" $LIBUSER_CONF elif grep -qs "\[defaults]" $LIBUSER_CONF ; then sed -i "/[[:space:]]*\[defaults]/a crypt_style = $var_password_hashing_algorithm_pam" $LIBUSER_CONF else echo -e "[defaults]\ncrypt_style = $var_password_hashing_algorithm_pam" >> $LIBUSER_CONF fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - set_password_hashing_algorithm_libuserconf - name: XCCDF Value var_password_hashing_algorithm_pam # promote to variable set_fact: var_password_hashing_algorithm_pam: !!str tags: - always - name: Set Password Hashing Algorithm in /etc/libuser.conf - Set Password Hashing Algorithm in /etc/libuser.conf ansible.builtin.lineinfile: dest: /etc/libuser.conf insertafter: ^\s*\[defaults] regexp: ^#?crypt_style line: crypt_style = {{ var_password_hashing_algorithm_pam }} state: present create: true when: '"libuser" in ansible_facts.packages' tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - set_password_hashing_algorithm_libuserconf Set Password Hashing Algorithm in /etc/login.defs In /etc/login.defs, add or update the following line to ensure the system will use as the hashing algorithm: ENCRYPT_METHOD 1 12 15 16 5 5.6.2.2 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.13.11 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0418 1055 1402 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(c) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.1 SRG-OS-000073-GPOS-00041 5.4.1.4 8.3.2 8.3 Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text. Using a stronger hashing algorithm makes password cracking attacks more difficult. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then var_password_hashing_algorithm='' # Allow multiple algorithms, but choose the first one for remediation # var_password_hashing_algorithm="$(echo $var_password_hashing_algorithm | cut -d \| -f 1)" # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ENCRYPT_METHOD") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "$var_password_hashing_algorithm" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^ENCRYPT_METHOD\\>" "/etc/login.defs"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^ENCRYPT_METHOD\\>.*/$escaped_formatted_output/gi" "/etc/login.defs" else if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs" fi printf '%s\n' "$formatted_output" >> "/etc/login.defs" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - set_password_hashing_algorithm_logindefs - name: XCCDF Value var_password_hashing_algorithm # promote to variable set_fact: var_password_hashing_algorithm: !!str tags: - always - name: Set Password Hashing Algorithm in /etc/login.defs ansible.builtin.lineinfile: dest: /etc/login.defs regexp: ^#?ENCRYPT_METHOD line: ENCRYPT_METHOD {{ var_password_hashing_algorithm.split('|')[0] }} state: present create: true when: '"shadow-utils" in ansible_facts.packages' tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - set_password_hashing_algorithm_logindefs Set PAM''s Password Hashing Algorithm - password-auth The PAM system service can be configured to only store encrypted representations of passwords. In /etc/pam.d/password-auth, the password section of the file controls which PAM modules to execute during a password change. Set the pam_unix.so module in the password section to include the option and no other hashing algorithms as shown below: password sufficient pam_unix.so other arguments... This will help ensure that new passwords for local users will be stored using the algorithm. The hashing algorithms to be used with pam_unix.so are defined with independent module options. There are at least 7 possible algorithms and likely more algorithms will be introduced along the time. Due the the number of options and its possible combinations, the use of multiple hashing algorithm options may bring unexpected behaviors to the system. For this reason the check will pass only when one hashing algorithm option is defined and is aligned to the "var_password_hashing_algorithm_pam" variable. The remediation will ensure the correct option and remove any other extra hashing algorithm option. 1 12 15 16 5 5.6.2.2 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.13.11 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0418 1055 1402 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(c) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.1 SRG-OS-000073-GPOS-00041 SRG-OS-000120-GPOS-00061 5.3.3.4.3 Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text. This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the crypt_style configuration option in /etc/libuser.conf ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_password_hashing_algorithm_pam='' PAM_FILE_PATH="/etc/pam.d/password-auth" if [ -e "$PAM_FILE_PATH" ] ; then PAM_FILE_PATH="$PAM_FILE_PATH" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$PAM_FILE_PATH") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_unix.so.*)/\1sufficient \2/" "$PAM_FILE_PATH" else echo "password sufficient pam_unix.so" >> "$PAM_FILE_PATH" fi fi # Check the option if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*\s$var_password_hashing_algorithm_pam\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+sufficient\s+pam_unix.so.*/ s/$/ $var_password_hashing_algorithm_pam/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$PAM_FILE_PATH was not found" >&2 fi # Ensure only the correct hashing algorithm option is used. declare -a HASHING_ALGORITHMS_OPTIONS=("sha512" "yescrypt" "gost_yescrypt" "blowfish" "sha256" "md5" "bigcrypt") for hash_option in "${HASHING_ALGORITHMS_OPTIONS[@]}"; do if [ "$hash_option" != "$var_password_hashing_algorithm_pam" ]; then if grep -qP "^\s*password\s+.*\s+pam_unix.so\s+.*\b$hash_option\b" "$PAM_FILE_PATH"; then if [ -e "$PAM_FILE_PATH" ] ; then PAM_FILE_PATH="$PAM_FILE_PATH" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$PAM_FILE_PATH") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*password\s+.*\s+pam_unix.so\s.*\b$hash_option\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*password.*.*.*pam_unix.so.*)\b$hash_option\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$PAM_FILE_PATH was not found" >&2 fi fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_passwordauth - name: XCCDF Value var_password_hashing_algorithm_pam # promote to variable set_fact: var_password_hashing_algorithm_pam: !!str tags: - always - name: Set PAM's Password Hashing Algorithm - password-auth - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_passwordauth - name: Set PAM's Password Hashing Algorithm - password-auth - Check the proper remediation for the system block: - name: Set PAM's Password Hashing Algorithm - password-auth - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Set PAM's Password Hashing Algorithm - password-auth - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect custom profile is used if authselect is present block: - name: Set PAM's Password Hashing Algorithm - password-auth - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set PAM's Password Hashing Algorithm - password-auth - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set PAM's Password Hashing Algorithm - password-auth - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set PAM's Password Hashing Algorithm - password-auth - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - password-auth - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - password-auth - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set PAM's Password Hashing Algorithm - password-auth - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set PAM's Password Hashing Algorithm - password-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set PAM's Password Hashing Algorithm - password-auth - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Set PAM's Password Hashing Algorithm - password-auth - Include or update the PAM module line in {{ pam_file_path }} block: - name: Set PAM's Password Hashing Algorithm - password-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_unix.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_unix.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' line: password {{ pam_module_control }} pam_unix.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Set PAM's Password Hashing Algorithm - password-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set PAM's Password Hashing Algorithm - password-auth - Check if the required PAM module option is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*\s{{ var_password_hashing_algorithm_pam }}\b state: absent check_mode: true changed_when: false register: result_pam_module_set_password_hashing_algorithm_passwordauth_option_present - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the "{{ var_password_hashing_algorithm_pam }}" PAM option for "pam_unix.so" is included in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so.*) line: \1 {{ var_password_hashing_algorithm_pam }} state: present register: result_pam_set_password_hashing_algorithm_passwordauth_add when: - result_pam_module_set_password_hashing_algorithm_passwordauth_option_present.found is defined - result_pam_module_set_password_hashing_algorithm_passwordauth_option_present.found == 0 - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - |- (result_pam_set_password_hashing_algorithm_passwordauth_add is defined and result_pam_set_password_hashing_algorithm_passwordauth_add.changed) or (result_pam_set_password_hashing_algorithm_passwordauth_edit is defined and result_pam_set_password_hashing_algorithm_passwordauth_edit.changed) when: - '"pam" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_passwordauth - name: Set PAM's Password Hashing Algorithm - password-auth - Check if /etc/pam.d/password-auth File is Present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_passwordauth - name: Set PAM's Password Hashing Algorithm - password-auth - Check The Proper Remediation For The System block: - name: Set PAM's Password Hashing Algorithm - password-auth - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Set PAM's Password Hashing Algorithm - password-auth - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect custom profile is used if authselect is present block: - name: Set PAM's Password Hashing Algorithm - password-auth - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set PAM's Password Hashing Algorithm - password-auth - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set PAM's Password Hashing Algorithm - password-auth - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set PAM's Password Hashing Algorithm - password-auth - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - password-auth - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - password-auth - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - password-auth - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set PAM's Password Hashing Algorithm - password-auth - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set PAM's Password Hashing Algorithm - password-auth - Check if "{{ pam_file_path }}" File is Present ansible.builtin.stat: path: '{{ pam_file_path }}' register: pam_file_path_present - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure That Only the Correct Hashing Algorithm Option For pam_unix.so Is Used in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (^\s*password.*pam_unix\.so.*)\b{{ item }}\b\s*(.*) replace: \1\2 when: - item != var_password_hashing_algorithm_pam - pam_file_path_present.stat.exists loop: - sha512 - yescrypt - gost_yescrypt - blowfish - sha256 - md5 - bigcrypt register: result_pam_hashing_options_removal - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_hashing_options_removal is changed when: - '"pam" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_passwordauth Set PAM''s Password Hashing Algorithm The PAM system service can be configured to only store encrypted representations of passwords. In "/etc/pam.d/system-auth", the password section of the file controls which PAM modules to execute during a password change. Set the pam_unix.so module in the password section to include the option and no other hashing algorithms as shown below: password sufficient pam_unix.so other arguments... This will help ensure that new passwords for local users will be stored using the algorithm. The hashing algorithms to be used with pam_unix.so are defined with independent module options. There are at least 7 possible algorithms and likely more algorithms will be introduced along the time. Due the the number of options and its possible combinations, the use of multiple hashing algorithm options may bring unexpected behaviors to the system. For this reason the check will pass only when one hashing algorithm option is defined and is aligned to the "var_password_hashing_algorithm_pam" variable. The remediation will ensure the correct option and remove any other extra hashing algorithm option. 1 12 15 16 5 5.6.2.2 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.13.11 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0418 1055 1402 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(c) IA-5(1)(c) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.1 SRG-OS-000073-GPOS-00041 SRG-OS-000120-GPOS-00061 R68 5.3.3.4.3 8.3.2 8.3 Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text. This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the crypt_style configuration option in /etc/libuser.conf ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_password_hashing_algorithm_pam='' PAM_FILE_PATH="/etc/pam.d/system-auth" # Ensure all the hashing algorithm option is removed. declare -a HASHING_ALGORITHMS_OPTIONS=("sha512" "yescrypt" "gost_yescrypt" "blowfish" "sha256" "md5" "bigcrypt") for hash_option in "${HASHING_ALGORITHMS_OPTIONS[@]}"; do if grep -qP "^\s*password\s+.*\s+pam_unix.so\s+.*\b$hash_option\b" "$PAM_FILE_PATH"; then if [ -e "$PAM_FILE_PATH" ] ; then PAM_FILE_PATH="$PAM_FILE_PATH" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$PAM_FILE_PATH") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if grep -qP "^\s*password\s+.*\s+pam_unix.so\s.*\b$hash_option\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "s/(.*password.*.*.*pam_unix.so.*)\b$hash_option\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$PAM_FILE_PATH was not found" >&2 fi fi done if [ -e "$PAM_FILE_PATH" ] ; then PAM_FILE_PATH="$PAM_FILE_PATH" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "$PAM_FILE_PATH") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_unix.so.*)/\1sufficient \2/" "$PAM_FILE_PATH" else echo "password sufficient pam_unix.so" >> "$PAM_FILE_PATH" fi fi # Check the option if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*\s$var_password_hashing_algorithm_pam\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+sufficient\s+pam_unix.so.*/ s/$/ $var_password_hashing_algorithm_pam/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "$PAM_FILE_PATH was not found" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_systemauth - name: XCCDF Value var_password_hashing_algorithm_pam # promote to variable set_fact: var_password_hashing_algorithm_pam: !!str tags: - always - name: Set PAM's Password Hashing Algorithm - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_systemauth - name: Set PAM's Password Hashing Algorithm - Check the proper remediation for the system block: - name: Set PAM's Password Hashing Algorithm - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Set PAM's Password Hashing Algorithm - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set PAM's Password Hashing Algorithm - Ensure authselect custom profile is used if authselect is present block: - name: Set PAM's Password Hashing Algorithm - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set PAM's Password Hashing Algorithm - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set PAM's Password Hashing Algorithm - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set PAM's Password Hashing Algorithm - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set PAM's Password Hashing Algorithm - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set PAM's Password Hashing Algorithm - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set PAM's Password Hashing Algorithm - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set PAM's Password Hashing Algorithm - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set PAM's Password Hashing Algorithm - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Set PAM's Password Hashing Algorithm - Include or update the PAM module line in {{ pam_file_path }} block: - name: Set PAM's Password Hashing Algorithm - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_unix.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Set PAM's Password Hashing Algorithm - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_unix.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Set PAM's Password Hashing Algorithm - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' line: password {{ pam_module_control }} pam_unix.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Set PAM's Password Hashing Algorithm - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set PAM's Password Hashing Algorithm - Check if the required PAM module option is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*\s{{ var_password_hashing_algorithm_pam }}\b state: absent check_mode: true changed_when: false register: result_pam_module_set_password_hashing_algorithm_systemauth_option_present - name: Set PAM's Password Hashing Algorithm - Ensure the "{{ var_password_hashing_algorithm_pam }}" PAM option for "pam_unix.so" is included in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so.*) line: \1 {{ var_password_hashing_algorithm_pam }} state: present register: result_pam_set_password_hashing_algorithm_systemauth_add when: - result_pam_module_set_password_hashing_algorithm_systemauth_option_present.found is defined - result_pam_module_set_password_hashing_algorithm_systemauth_option_present.found == 0 - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - |- (result_pam_set_password_hashing_algorithm_systemauth_add is defined and result_pam_set_password_hashing_algorithm_systemauth_add.changed) or (result_pam_set_password_hashing_algorithm_systemauth_edit is defined and result_pam_set_password_hashing_algorithm_systemauth_edit.changed) when: - '"pam" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_systemauth - name: Set PAM's Password Hashing Algorithm - Check if /etc/pam.d/system-auth File is Present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present when: '"pam" in ansible_facts.packages' tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_systemauth - name: Set PAM's Password Hashing Algorithm - Check The Proper Remediation For The System block: - name: Set PAM's Password Hashing Algorithm - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Set PAM's Password Hashing Algorithm - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set PAM's Password Hashing Algorithm - Ensure authselect custom profile is used if authselect is present block: - name: Set PAM's Password Hashing Algorithm - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set PAM's Password Hashing Algorithm - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set PAM's Password Hashing Algorithm - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set PAM's Password Hashing Algorithm - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set PAM's Password Hashing Algorithm - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set PAM's Password Hashing Algorithm - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set PAM's Password Hashing Algorithm - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set PAM's Password Hashing Algorithm - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set PAM's Password Hashing Algorithm - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set PAM's Password Hashing Algorithm - Check if "{{ pam_file_path }}" File is Present ansible.builtin.stat: path: '{{ pam_file_path }}' register: pam_file_path_present - name: Set PAM's Password Hashing Algorithm - Ensure That Only the Correct Hashing Algorithm Option For pam_unix.so Is Used in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: (^\s*password.*pam_unix\.so.*)\b{{ item }}\b\s*(.*) replace: \1\2 when: - item != var_password_hashing_algorithm_pam - pam_file_path_present.stat.exists loop: - sha512 - yescrypt - gost_yescrypt - blowfish - sha256 - md5 - bigcrypt register: result_pam_hashing_options_removal - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - result_pam_hashing_options_removal is changed when: - '"pam" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - CJIS-5.6.2.2 - NIST-800-171-3.13.11 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.1 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - set_password_hashing_algorithm_systemauth Protect Physical Console Access It is impossible to fully protect a system from an attacker with physical access, so securing the space in which the system is located should be considered a necessary step. However, there are some steps which, if taken, make it more difficult for an attacker to quickly or undetectably modify a system from its console. Disable debug-shell SystemD Service SystemD's debug-shell service is intended to diagnose SystemD related boot issues with various systemctl commands. Once enabled and following a system reboot, the root shell will be available on tty9 which is access by pressing CTRL-ALT-F9. The debug-shell service should only be used for SystemD related issues and should otherwise be disabled. By default, the debug-shell SystemD service is already disabled. The debug-shell service can be disabled with the following command: $ sudo systemctl mask --now debug-shell.service 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) CM-6 FIA_UAU.1 SRG-OS-000324-GPOS-00125 SRG-OS-000480-GPOS-00227 This prevents attackers with physical access from trivially bypassing security on the machine through valid troubleshooting configurations and gaining root access when the system is rebooted. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'debug-shell.service' fi "$SYSTEMCTL_EXEC" disable 'debug-shell.service' "$SYSTEMCTL_EXEC" mask 'debug-shell.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files debug-shell.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'debug-shell.socket' fi "$SYSTEMCTL_EXEC" mask 'debug-shell.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'debug-shell.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.5 - NIST-800-53-CM-6 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_debug-shell_disabled - name: Disable debug-shell SystemD Service - Disable service debug-shell block: - name: Disable debug-shell SystemD Service - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable debug-shell SystemD Service - Ensure debug-shell.service is Masked ansible.builtin.systemd: name: debug-shell.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("debug-shell.service", multiline=True) - name: Unit Socket Exists - debug-shell.socket ansible.builtin.command: systemctl -q list-unit-files debug-shell.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable debug-shell SystemD Service - Disable Socket debug-shell ansible.builtin.systemd: name: debug-shell.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("debug-shell.socket", multiline=True) tags: - NIST-800-171-3.4.5 - NIST-800-53-CM-6 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_debug-shell_disabled - special_service_block when: '"kernel" in ansible_facts.packages' include disable_debug-shell class disable_debug-shell { service {'debug-shell': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: debug-shell.service enabled: false mask: true - name: debug-shell.socket enabled: false mask: true [customizations.services] masked = ["debug-shell"] service disable debug-shell Disable Ctrl-Alt-Del Reboot Activation By default, SystemD will reboot the system if the Ctrl-Alt-Del key sequence is pressed. To configure the system to ignore the Ctrl-Alt-Del key sequence from the command line instead of rebooting the system, do either of the following: ln -sf /dev/null /etc/systemd/system/ctrl-alt-del.target or systemctl mask ctrl-alt-del.target Do not simply delete the /usr/lib/systemd/system/ctrl-alt-del.service file, as this file may be restored during future system updates. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 FAU_GEN.1.2 SRG-OS-000324-GPOS-00125 SRG-OS-000480-GPOS-00227 A locally logged-in user who presses Ctrl-Alt-Del, when at the console, can reboot the system. If accidentally pressed, as could happen in the case of mixed OS environment, this can create the risk of short-term loss of availability of systems due to unintentional reboot. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then systemctl disable ctrl-alt-del.target systemctl mask ctrl-alt-del.target else systemctl disable --now ctrl-alt-del.target systemctl mask --now ctrl-alt-del.target fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_ctrlaltdel_reboot - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - name: Disable Ctrl-Alt-Del Reboot Activation ansible.builtin.systemd: name: ctrl-alt-del.target force: true masked: true state: stopped when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_ctrlaltdel_reboot - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: ctrl-alt-del.target mask: true Verify that Interactive Boot is Disabled Fedora systems support an "interactive boot" option that can be used to prevent services from being started. On a Fedora system, interactive boot can be enabled by providing a 1, yes, true, or on value to the systemd.confirm_spawn kernel argument in /etc/default/grub. Remove any instance of systemd.confirm_spawn=(1|yes|true|on) from the kernel arguments in that file to disable interactive boot. Recovery booting must also be disabled. Confirm that GRUB_DISABLE_RECOVERY=true is set in /etc/default/grub. It is also required to change the runtime configuration, run: /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn" grub2-mkconfig -o /boot/grub2/grub.cfg 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 3.1.2 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 SC-2(1) CM-6(a) PR.AC-4 PR.AC-6 PR.PT-3 SRG-OS-000480-GPOS-00227 Using interactive or recovery boot, the console user could disable auditing, firewalls, or other services, weakening system security. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q grub2-common; }; then # Verify that Interactive Boot is Disabled in /etc/default/grub CONFIRM_SPAWN_YES="systemd.confirm_spawn\(=\(1\|yes\|true\|on\)\|\b\)" CONFIRM_SPAWN_NO="systemd.confirm_spawn=no" if grep -q "\(GRUB_CMDLINE_LINUX\|GRUB_CMDLINE_LINUX_DEFAULT\)" /etc/default/grub then sed -i "s/${CONFIRM_SPAWN_YES}/${CONFIRM_SPAWN_NO}/" /etc/default/grub fi # make sure GRUB_DISABLE_RECOVERY=true if grep -q '^GRUB_DISABLE_RECOVERY=.*' '/etc/default/grub' ; then # modify the GRUB command-line if an GRUB_DISABLE_RECOVERY= arg already exists sed -i 's/GRUB_DISABLE_RECOVERY=.*/GRUB_DISABLE_RECOVERY=true/' /etc/default/grub else # no GRUB_DISABLE_RECOVERY=arg is present, append it to file echo "GRUB_DISABLE_RECOVERY=true" >> '/etc/default/grub' fi # Remove 'systemd.confirm_spawn' kernel argument also from runtime settings /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn" #Regen grub.cfg handle updated GRUB_DISABLE_RECOVERY and confirm_spawn grub2-mkconfig -o /boot/grub2/grub.cfg else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.2 - NIST-800-171-3.4.5 - NIST-800-53-CM-6(a) - NIST-800-53-SC-2(1) - grub2_disable_interactive_boot - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Verify GRUB_DISABLE_RECOVERY=true ansible.builtin.lineinfile: path: /etc/default/grub regexp: ^GRUB_DISABLE_RECOVERY=.* line: GRUB_DISABLE_RECOVERY=true state: present when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.4.5 - NIST-800-53-CM-6(a) - NIST-800-53-SC-2(1) - grub2_disable_interactive_boot - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Verify that Interactive Boot is Disabled in /etc/default/grub ansible.builtin.replace: dest: /etc/default/grub regexp: systemd.confirm_spawn(=(1|yes|true|on)|\b) replace: systemd.confirm_spawn=no when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.4.5 - NIST-800-53-CM-6(a) - NIST-800-53-SC-2(1) - grub2_disable_interactive_boot - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Verify that Interactive Boot is Disabled (runtime) ansible.builtin.command: /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn" when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.4.5 - NIST-800-53-CM-6(a) - NIST-800-53-SC-2(1) - grub2_disable_interactive_boot - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Regen grub.cfg handle updated GRUB_DISABLE_RECOVERY and confirm_spawn ansible.builtin.command: grub2-mkconfig -o /boot/grub2/grub.cfg when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.4.5 - NIST-800-53-CM-6(a) - NIST-800-53-SC-2(1) - grub2_disable_interactive_boot - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Require Authentication for Emergency Systemd Target Emergency mode is intended as a system recovery method, providing a single user root access to the system during a failed boot sequence. By default, Emergency mode is protected by requiring a password and is set in /usr/lib/systemd/system/emergency.service. 1 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.06 DSS06.10 3.1.1 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 IA-2 AC-3 CM-6(a) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.PT-3 SRG-OS-000080-GPOS-00048 This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then service_dropin_cfg_dir="/etc/systemd/system/emergency.service.d" service_dropin_file="${service_dropin_cfg_dir}/10-oscap.conf" sulogin="/usr/lib/systemd/systemd-sulogin-shell emergency" mkdir -p "${service_dropin_cfg_dir}" echo "[Service]" >> "${service_dropin_file}" echo "ExecStart=-$sulogin" >> "${service_dropin_file}" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.1 - NIST-800-171-3.4.5 - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - require_emergency_target_auth - restrict_strategy - name: Require emergency mode password ansible.builtin.blockinfile: create: true dest: /etc/systemd/system/emergency.service.d/10-oscap.conf block: |- [Service] ExecStart=-/usr/lib/systemd/systemd-sulogin-shell emergency when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.1 - NIST-800-171-3.4.5 - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - require_emergency_target_auth - restrict_strategy Require Authentication for Single User Mode Single-user mode is intended as a system recovery method, providing a single user root access to the system by providing a boot option at startup. By default, single-user mode is protected by requiring a password and is set in /usr/lib/systemd/system/rescue.service. 1 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.06 DSS06.10 3.1.1 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 IA-2 AC-3 CM-6(a) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.PT-3 FIA_UAU.1 SRG-OS-000080-GPOS-00048 This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/systemd/system/rescue.service.d/10-oscap.conf /etc/systemd/system/rescue.service.d/*.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[Service\]([^\n\[]*\n+)+?[[:space:]]*ExecStart" "$f"; then sed -i "s/ExecStart[^(\n)]*/ExecStart=\nExecStart=-\/usr\/lib\/systemd\/systemd-sulogin-shell rescue/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[Service\]" "$f"; then sed -i "/[[:space:]]*\[Service\]/a ExecStart=\nExecStart=-\/usr\/lib\/systemd\/systemd-sulogin-shell rescue" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/systemd/system/rescue.service.d/10-oscap.conf /etc/systemd/system/rescue.service.d/*.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[Service]\nExecStart=\nExecStart=-/usr/lib/systemd/systemd-sulogin-shell rescue" >> "$file" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.1 - NIST-800-171-3.4.5 - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - require_singleuser_auth - restrict_strategy - name: Require Authentication for Single User Mode - find files which already override Execstart of rescue.service ansible.builtin.find: paths: /etc/systemd/system/rescue.service.d patterns: '*.conf' contains: ^\s*ExecStart=.*$ register: rescue_service_overrides_found when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.1 - NIST-800-171-3.4.5 - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - require_singleuser_auth - restrict_strategy - name: Require Authentication for Single User Mode - set files containing ExecStart overrides as target ansible.builtin.set_fact: rescue_service_remediation_target_file: '{{ rescue_service_overrides_found.files | map(attribute=''path'') | list }}' when: - '"kernel" in ansible_facts.packages' - rescue_service_overrides_found.matched is defined and rescue_service_overrides_found.matched > 0 tags: - NIST-800-171-3.1.1 - NIST-800-171-3.4.5 - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - require_singleuser_auth - restrict_strategy - name: Require Authentication for Single User Mode - set default target for rescue.service override ansible.builtin.set_fact: rescue_service_remediation_target_file: - /etc/systemd/system/rescue.service.d/10-oscap.conf when: - '"kernel" in ansible_facts.packages' - rescue_service_overrides_found.matched is defined and rescue_service_overrides_found.matched == 0 tags: - NIST-800-171-3.1.1 - NIST-800-171-3.4.5 - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - require_singleuser_auth - restrict_strategy - name: Require Authentication for Single User Mode - Require emergency user mode password community.general.ini_file: path: '{{ item }}' section: Service option: ExecStart values: - '' - -/usr/lib/systemd/systemd-sulogin-shell rescue loop: '{{ rescue_service_remediation_target_file }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.1 - NIST-800-171-3.4.5 - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - require_singleuser_auth - restrict_strategy Configure Screen Locking When a user must temporarily leave an account logged-in, screen locking should be employed to prevent passersby from abusing the account. User education and training is particularly important for screen locking to be effective, and policies can be implemented to reinforce this. Automatic screen locking is only meant as a safeguard for those cases where a user forgot to lock the screen. Configure Console Screen Locking A console screen locking mechanism is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operation system session prior to vacating the vicinity, operating systems need to be able to identify when a user's session has idled and take action to initiate the session lock. Install the screen Package To enable console screen locking, install the screen package. The screen package can be installed with the following command: $ sudo dnf install screen Instruct users to begin new terminal sessions with the following command: $ screen The console can now be locked with the following key combination: ctrl+a x 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) PR.AC-7 FMT_MOF_EXT.1 SRG-OS-000029-GPOS-00010 A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operation system session prior to vacating the vicinity, operating systems need to be able to identify when a user's session has idled and take action to initiate the session lock. The screen package allows for a session lock to be implemented and configured. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "screen" ; then dnf install -y "screen" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_screen_installed - name: Ensure screen is installed ansible.builtin.package: name: screen state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_screen_installed include install_screen class install_screen { package { 'screen': ensure => 'installed', } } package --add=screen [[packages]] name = "screen" version = "*" package install screen dnf install screen Install the tmux Package To enable console screen locking, install the tmux package. The tmux package can be installed with the following command: $ sudo dnf install tmux A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to log out because of the temporary nature of the absence. The session lock is implemented at the point where session activity can be determined. Rather than be forced to wait for a period of time to expire before the user session can be locked, Fedora needs to provide users with the ability to manually invoke a session lock so users can secure their session if it is necessary to temporarily vacate the immediate physical vicinity. Instruct users to begin new terminal sessions with the following command: $ tmux The console can now be locked with the following key combination: ctrl+b :lock-session 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) PR.AC-7 FMT_SMF_EXT.1 FMT_MOF_EXT.1 FTA_SSL.1 SRG-OS-000030-GPOS-00011 SRG-OS-000028-GPOS-00009 A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operation system session prior to vacating the vicinity, operating systems need to be able to identify when a user's session has idled and take action to initiate the session lock. The tmux package allows for a session lock to be implemented and configured. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "tmux" ; then dnf install -y "tmux" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_tmux_installed - name: Ensure tmux is installed ansible.builtin.package: name: tmux state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.10 - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_tmux_installed include install_tmux class install_tmux { package { 'tmux': ensure => 'installed', } } package --add=tmux [[packages]] name = "tmux" version = "*" package install tmux dnf install tmux Support session locking with tmux The tmux terminal multiplexer is used to implement automatic session locking. It should be started from /etc/bashrc or drop-in files within /etc/profile.d/. FMT_SMF_EXT.1 FMT_MOF_EXT.1 FTA_SSL.1 SRG-OS-000031-GPOS-00012 SRG-OS-000028-GPOS-00009 SRG-OS-000030-GPOS-00011 Unlike bash itself, the tmux terminal multiplexer provides a mechanism to lock sessions after period of inactivity. A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to log out because of the temporary nature of the absence. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q tmux; }; then if ! grep -x ' case "$name" in (sshd|login) exec tmux ;; esac' /etc/bashrc; then cat >> /etc/profile.d/tmux.sh <<'EOF' if [ "$PS1" ]; then parent=$(ps -o ppid= -p $$) name=$(ps -o comm= -p $parent) case "$name" in (sshd|login) exec tmux ;; esac fi EOF chmod 0644 /etc/profile.d/tmux.sh fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_bashrc_exec_tmux - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: 'Support session locking with tmux: Determine If the Tmux Launch Script Is Present in /etc/bashrc' ansible.builtin.find: paths: /etc patterns: bashrc contains: .*case "$name" in sshd|login\) exec tmux ;; esac.* register: tmux_in_bashrc when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' tags: - configure_bashrc_exec_tmux - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: 'Support session locking with tmux: Determine If the Tmux Launch Script Is Present in /etc/profile.d/*.sh' ansible.builtin.find: paths: /etc/profile.d patterns: '*.sh' contains: .*case "$name" in sshd|login\) exec tmux ;; esac.* register: tmux_in_profile_d when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' tags: - configure_bashrc_exec_tmux - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: 'Support session locking with tmux: Insert the Correct Script into /etc/profile.d/tmux.sh' ansible.builtin.blockinfile: path: /etc/profile.d/tmux.sh block: | if [ "$PS1" ]; then parent=$(ps -o ppid= -p $$) name=$(ps -o comm= -p $parent) case "$name" in sshd|login) exec tmux ;; esac fi create: true when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' - tmux_in_bashrc is defined and tmux_in_bashrc.matched == 0 - tmux_in_profile_d is defined and tmux_in_profile_d.matched == 0 tags: - configure_bashrc_exec_tmux - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Configure tmux to lock session after inactivity To enable console screen locking in tmux terminal multiplexer after a period of inactivity, the lock-after-time option has to be set to a value greater than 0 and less than or equal to 900 in /etc/tmux.conf. FMT_SMF_EXT.1 FMT_MOF_EXT.1 FTA_SSL.1 SRG-OS-000029-GPOS-00010 SRG-OS-000031-GPOS-00012 Locking the session after a period of inactivity limits the potential exposure if the session is left unattended. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q tmux; }; then tmux_conf="/etc/tmux.conf" if grep -qP '^\s*set\s+-g\s+lock-after-time' "$tmux_conf" ; then sed -i 's/^\s*set\s\+-g\s\+lock-after-time.*$/set -g lock-after-time 900/' "$tmux_conf" else echo "set -g lock-after-time 900" >> "$tmux_conf" fi chmod 0644 "$tmux_conf" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_tmux_lock_after_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure tmux to lock session after inactivity block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)^\s*set -g lock-after-time\s+ mode: '0644' state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/tmux.conf ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)^\s*set -g lock-after-time\s+ mode: '0644' state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/tmux.conf ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)^\s*set -g lock-after-time\s+ mode: '0644' line: set -g lock-after-time 900 state: present when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' tags: - configure_tmux_lock_after_time - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Configure the tmux Lock Command To enable console screen locking in tmux terminal multiplexer, the vlock command must be configured to be used as a locking mechanism. Add the following line to /etc/tmux.conf: set -g lock-command vlock. The console can now be locked with the following key combination: ctrl+b :lock-session AC-11(a) AC-11(b) CM-6(a) FMT_SMF_EXT.1 FMT_MOF_EXT.1 FTA_SSL.1 SRG-OS-000028-GPOS-00009 SRG-OS-000030-GPOS-00011 The tmux package allows for a session lock to be implemented and configured. However, the session lock is implemented by an external command. The tmux default configuration does not contain an effective session lock. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q tmux; }; then tmux_conf="/etc/tmux.conf" if grep -qP '^\s*set\s+-g\s+lock-command' "$tmux_conf" ; then sed -i 's/^\s*set\s\+-g\s\+lock-command.*$/set -g lock-command vlock/' "$tmux_conf" else echo "set -g lock-command vlock" >> "$tmux_conf" fi chmod 0644 "$tmux_conf" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-11(a) - NIST-800-53-AC-11(b) - NIST-800-53-CM-6(a) - configure_tmux_lock_command - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure the tmux Lock Command block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)^\s*set -g lock-command\s+ mode: '0644' state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/tmux.conf ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)^\s*set -g lock-command\s+ mode: '0644' state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/tmux.conf ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)^\s*set -g lock-command\s+ mode: '0644' line: set -g lock-command vlock state: present when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' tags: - NIST-800-53-AC-11(a) - NIST-800-53-AC-11(b) - NIST-800-53-CM-6(a) - configure_tmux_lock_command - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Configure the tmux lock session key binding To set a key binding for the screen locking in tmux terminal multiplexer, the session-lock command must be bound to a key. Add the following line to /etc/tmux.conf: bind X lock-session. The console can now be locked with the following key combination: Ctrl+b Shift+x SRG-OS-000028-GPOS-00009 SRG-OS-000030-GPOS-00011 The tmux package allows for a session lock to be implemented and configured. However, the session lock is implemented by an external command. The tmux default configuration does not contain an effective session lock. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q tmux; }; then tmux_conf="/etc/tmux.conf" if ! grep -qP '^\s*bind\s+\w\s+lock-session' "$tmux_conf" ; then echo "bind X lock-session" >> "$tmux_conf" fi chmod 0644 "$tmux_conf" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - configure_tmux_lock_keybinding - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)\s*bind\s+\w\s+lock-session.*$ mode: '0644' state: absent check_mode: true changed_when: false register: dupes when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' tags: - configure_strategy - configure_tmux_lock_keybinding - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Deduplicate values from /etc/tmux.conf ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)\s*bind\s+\w\s+lock-session.*$ mode: '0644' state: absent when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' - dupes.found is defined and dupes.found > 1 tags: - configure_strategy - configure_tmux_lock_keybinding - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Insert correct line into /etc/tmux.conf ansible.builtin.lineinfile: path: /etc/tmux.conf create: true regexp: (?i)\s*bind\s+\w\s+lock-session.*$ mode: '0644' line: bind X lock-session state: present when: - '"kernel" in ansible_facts.packages' - '"tmux" in ansible_facts.packages' tags: - configure_strategy - configure_tmux_lock_keybinding - low_complexity - low_disruption - low_severity - no_reboot_needed Prevent user from disabling the screen lock The tmux terminal multiplexer is used to implement automatic session locking. It should not be listed in /etc/shells. CM-6 FMT_SMF_EXT.1 FMT_MOF_EXT.1 FTA_SSL.1 SRG-OS-000324-GPOS-00125 SRG-OS-000028-GPOS-00009 SRG-OS-000030-GPOS-00011 Not listing tmux among permitted shells prevents malicious program running as user from lowering security by disabling the screen lock. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if grep -q 'tmux\s*$' /etc/shells ; then sed -i '/tmux\s*$/d' /etc/shells fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - low_complexity - low_disruption - low_severity - no_reboot_needed - no_tmux_in_shells - restrict_strategy - name: Prevent user from disabling the screen lock - Ensure tmux line not exists ansible.builtin.lineinfile: path: /etc/shells regex: tmux\s*$ state: absent when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - low_complexity - low_disruption - low_severity - no_reboot_needed - no_tmux_in_shells - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,/bin/sh%0A/bin/bash%0A/usr/bin/sh%0A/usr/bin/bash%0A mode: 0644 path: /etc/shells overwrite: true Hardware Tokens for Authentication The use of hardware tokens such as smart cards for system login provides stronger, two-factor authentication than using a username and password. In Red Hat Enterprise Linux servers and workstations, hardware token login is not enabled by default and must be enabled in the system settings. OpenSC Smart Card Drivers Choose the Smart Card Driver in use by your organization. For DoD, choose the cac driver. If your driver is not listed and you don't want to use the default driver, use the other option and manually specify your driver. default acos5 akis asepcos atrust-acos authentic belpic cac cardos coolkey cyberflex dnie entersafe epass2003 flex gemsafeV1 gids gpk iasecc incrypto34 isoApplet itacns jpki MaskTech mcrd muscle myeid npa oberthur openpgp None PIV-II rutoken_ecp rutoken sc-hsm setcos starcos tcos westcos Install the opensc Package For Multifactor Authentication The opensc package can be installed with the following command: $ sudo dnf install opensc 1382 1384 1386 CM-6(a) SRG-OS-000375-GPOS-00160 SRG-OS-000376-GPOS-00161 Using an authentication device, such as a CAC or token that is separate from the information system, ensures that even if the information system is compromised, that compromise will not affect credentials stored on the authentication device. Multifactor solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards or similar secure authentication devices issued by an organization or identity provider. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "opensc" ; then dnf install -y "opensc" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_opensc_installed - name: Ensure opensc is installed ansible.builtin.package: name: opensc state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_opensc_installed include install_opensc class install_opensc { package { 'opensc': ensure => 'installed', } } package --add=opensc [[packages]] name = "opensc" version = "*" package install opensc dnf install opensc Install the pcsc-lite package The pcsc-lite package can be installed with the following command: $ sudo dnf install pcsc-lite 1382 1384 1386 CM-6(a) SRG-OS-000375-GPOS-00160 The pcsc-lite package must be installed if it is to be available for multifactor authentication using smartcards. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "pcsc-lite" ; then dnf install -y "pcsc-lite" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_pcsc-lite_installed - name: Ensure pcsc-lite is installed ansible.builtin.package: name: pcsc-lite state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_pcsc-lite_installed include install_pcsc-lite class install_pcsc-lite { package { 'pcsc-lite': ensure => 'installed', } } package --add=pcsc-lite [[packages]] name = "pcsc-lite" version = "*" package install pcsc-lite dnf install pcsc-lite Install Smart Card Packages For Multifactor Authentication Configure the operating system to implement multifactor authentication by installing the required package with the following command: The openssl-pkcs11 package can be installed with the following command: $ sudo dnf install openssl-pkcs11 CM-6(a) Req-8.3 SRG-OS-000105-GPOS-00052 SRG-OS-000375-GPOS-00160 SRG-OS-000375-GPOS-00161 SRG-OS-000377-GPOS-00162 Using an authentication device, such as a CAC or token that is separate from the information system, ensures that even if the information system is compromised, that compromise will not affect credentials stored on the authentication device. Multifactor solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards or similar secure authentication devices issued by an organization or identity provider. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ); }; then if ! rpm -q --quiet "openssl-pkcs11" ; then dnf install -y "openssl-pkcs11" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.3 - enable_strategy - install_smartcard_packages - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure openssl-pkcs11 is installed ansible.builtin.package: name: openssl-pkcs11 state: present when: - '"kernel" in ansible_facts.packages' - ansible_architecture != "s390x" tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.3 - enable_strategy - install_smartcard_packages - low_complexity - low_disruption - medium_severity - no_reboot_needed include install_openssl-pkcs11 class install_openssl-pkcs11 { package { 'openssl-pkcs11': ensure => 'installed', } } package --add=openssl-pkcs11 [[packages]] name = "openssl-pkcs11" version = "*" package install openssl-pkcs11 dnf install openssl-pkcs11 Enable the pcscd Service The pcscd service can be enabled with the following command: $ sudo systemctl enable pcscd.service 1382 1384 1386 IA-2(1) IA-2(2) IA-2(3) IA-2(4) IA-2(6) IA-2(7) IA-2(11) CM-6(a) Req-8.3 SRG-OS-000375-GPOS-00160 Using an authentication device, such as a CAC or token that is separate from the information system, ensures that even if the information system is compromised, that compromise will not affect credentials stored on the authentication device. Multifactor solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards or similar secure authentication devices issued by an organization or identity provider. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'pcscd.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'pcscd.service' fi "$SYSTEMCTL_EXEC" enable 'pcscd.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_pcscd_enabled - name: Enable service pcscd block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Start service pcscd ansible.builtin.systemd: name: pcscd state: started masked: 'no' when: - '"pcsc-lite" in ansible_facts.packages' - name: Enable service pcscd ansible.builtin.command: cmd: systemctl enable pcscd when: - '"pcsc-lite" in ansible_facts.packages' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_pcscd_enabled include enable_pcscd class enable_pcscd { service {'pcscd': enable => true, ensure => 'running', } } [customizations.services] enabled = ["pcscd"] service enable pcscd Configure opensc Smart Card Drivers The OpenSC smart card tool can auto-detect smart card drivers; however, setting the smart card drivers in use by your organization helps to prevent users from using unauthorized smart cards. The default smart card driver for this profile is . To configure the OpenSC driver, edit the /etc/opensc.conf and add the following line into the file in the app default block, so it will look like: app default { ... card_drivers = ; } 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 1382 1384 1386 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-2(1) IA-2(2) IA-2(3) IA-2(4) IA-2(6) IA-2(7) IA-2(11) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.3 SRG-OS-000104-GPOS-00051 SRG-OS-000106-GPOS-00053 SRG-OS-000107-GPOS-00054 SRG-OS-000109-GPOS-00056 SRG-OS-000108-GPOS-00055 SRG-OS-000108-GPOS-00057 SRG-OS-000108-GPOS-00058 Smart card login provides two-factor authentication stronger than that provided by a username and password combination. Smart cards leverage PKI (public key infrastructure) in order to provide and verify credentials. Configuring the smart card driver in use by your organization helps to prevent users from using unauthorized smart cards. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_smartcard_drivers='' OPENSC_TOOL="/usr/bin/opensc-tool" if [ -f "${OPENSC_TOOL}" ]; then ${OPENSC_TOOL} -S app:default:card_drivers:$var_smartcard_drivers fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_opensc_card_drivers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_smartcard_drivers # promote to variable set_fact: var_smartcard_drivers: !!str tags: - always - name: Check existence of opensc conf ansible.builtin.stat: path: /etc/opensc-{{ ansible_architecture }}.conf register: opensc_conf_cd when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_opensc_card_drivers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure smartcard driver block block: - name: Check if card_drivers is defined ansible.builtin.command: /usr/bin/opensc-tool -G app:default:card_drivers changed_when: false register: card_drivers - name: Configure opensc Smart Card Drivers ansible.builtin.command: | /usr/bin/opensc-tool -S app:default:card_drivers:{{ var_smartcard_drivers }} when: - card_drivers.stdout != var_smartcard_drivers when: - '"kernel" in ansible_facts.packages' - opensc_conf_cd.stat.exists tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_opensc_card_drivers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Configure NSS DB To Use opensc The opensc module should be configured for use over the Coolkey PKCS#11 module in the NSS database. To configure the NSS database to use the opensc module, run the following command: $ sudo pkcs11-switch opensc NSS modules information are stored in NSS database which is in binary format. Currently it is not possible to check NSS database using OVAL. This is the reason there is no OVAL check for this rule. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-2(1) IA-2(2) IA-2(3) IA-2(4) IA-2(6) IA-2(7) IA-2(11) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.3 SRG-OS-000104-GPOS-00051 SRG-OS-000106-GPOS-00053 SRG-OS-000107-GPOS-00054 SRG-OS-000109-GPOS-00056 SRG-OS-000108-GPOS-00055 SRG-OS-000108-GPOS-00057 SRG-OS-000108-GPOS-00058 Smart card login provides two-factor authentication stronger than that provided by a username and password combination. Smart cards leverage PKI (public key infrastructure) in order to provide and verify credentials. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then PKCSSW=$(/usr/bin/pkcs11-switch) if [ ${PKCSSW} != "opensc" ] ; then echo -e "\n" | ${PKCSSW} opensc fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_opensc_nss_db - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure NSS DB To Use opensc - Check Existence of pkcs11-switch ansible.builtin.stat: path: /usr/bin/pkcs11-switch register: pkcs11switch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_opensc_nss_db - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure NSS DB To Use opensc - Get NSS Database Smart Card Configuration ansible.builtin.command: /usr/bin/pkcs11-switch changed_when: true register: pkcsw_output when: - '"kernel" in ansible_facts.packages' - pkcs11switch.stat.exists tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_opensc_nss_db - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure NSS DB To Use opensc - Select opensc Module ansible.builtin.shell: echo -e "\n" | /usr/bin/pkcs11-switch opensc when: - '"kernel" in ansible_facts.packages' - pkcs11switch.stat.exists and pkcsw_output.stdout != "opensc" tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_opensc_nss_db - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Force opensc To Use Defined Smart Card Driver The OpenSC smart card middleware can auto-detect smart card drivers; however by forcing the smart card driver in use by your organization, opensc will no longer autodetect or use other drivers unless specified. This helps to prevent users from using unauthorized smart cards. The default smart card driver for this profile is . To force the OpenSC driver, edit the /etc/opensc.conf. Look for a line similar to: # force_card_driver = customcos; and change it to: force_card_driver = ; 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 1382 1384 1386 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-2(1) IA-2(2) IA-2(3) IA-2(4) IA-2(6) IA-2(7) IA-2(11) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.3 SRG-OS-000104-GPOS-00051 SRG-OS-000106-GPOS-00053 SRG-OS-000107-GPOS-00054 SRG-OS-000109-GPOS-00056 SRG-OS-000108-GPOS-00055 SRG-OS-000108-GPOS-00057 SRG-OS-000108-GPOS-00058 Smart card login provides two-factor authentication stronger than that provided by a username and password combination. Smart cards leverage PKI (public key infrastructure) in order to provide and verify credentials. Forcing the smart card driver in use by your organization helps to prevent users from using unauthorized smart cards. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_smartcard_drivers='' OPENSC_TOOL="/usr/bin/opensc-tool" if [ -f "${OPENSC_TOOL}" ]; then ${OPENSC_TOOL} -S app:default:force_card_driver:$var_smartcard_drivers fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_strategy - force_opensc_card_drivers - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_smartcard_drivers # promote to variable set_fact: var_smartcard_drivers: !!str tags: - always - name: Check existence of opensc conf ansible.builtin.stat: path: /etc/opensc-{{ ansible_architecture }}.conf register: opensc_conf_fcd when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_strategy - force_opensc_card_drivers - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Force smartcard driver block block: - name: Check if force_card_driver is defined ansible.builtin.command: /usr/bin/opensc-tool -G app:default:force_card_driver changed_when: false register: force_card_driver - name: Force opensc To Use Defined Smart Card Driver ansible.builtin.command: | /usr/bin/opensc-tool -S app:default:force_card_driver:{{ var_smartcard_drivers }} when: - force_card_driver.stdout != var_smartcard_drivers when: - '"kernel" in ansible_facts.packages' - opensc_conf_fcd.stat.exists tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-2(1) - NIST-800-53-IA-2(11) - NIST-800-53-IA-2(2) - NIST-800-53-IA-2(3) - NIST-800-53-IA-2(4) - NIST-800-53-IA-2(6) - NIST-800-53-IA-2(7) - PCI-DSS-Req-8.3 - configure_strategy - force_opensc_card_drivers - low_complexity - low_disruption - medium_severity - no_reboot_needed Enable Smart Card Login To enable smart card authentication, consult the documentation at: For guidance on enabling SSH to authenticate against a Common Access Card (CAC), consult documentation at: https://access.redhat.com/solutions/82273 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-2(1) IA-2(2) IA-2(3) IA-2(4) IA-2(6) IA-2(7) IA-2(11) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.3 SRG-OS-000104-GPOS-00051 SRG-OS-000106-GPOS-00053 SRG-OS-000107-GPOS-00054 SRG-OS-000108-GPOS-00055 SRG-OS-000108-GPOS-00057 SRG-OS-000108-GPOS-00058 SRG-OS-000109-GPOS-00056 SRG-OS-000376-GPOS-00161 SRG-OS-000377-GPOS-00162 Smart card login provides two-factor authentication stronger than that provided by a username and password combination. Smart cards leverage PKI (public key infrastructure) in order to provide and verify credentials. Protect Accounts by Restricting Password-Based Login Conventionally, Unix shell accounts are accessed by providing a username and password to a login program, which tests these values for correctness using the /etc/passwd and /etc/shadow files. Password-based login is vulnerable to guessing of weak passwords, and to sniffing and man-in-the-middle attacks against passwords entered over a network or at an insecure console. Therefore, mechanisms for accessing accounts by entering usernames and passwords should be restricted to those which are operationally necessary. Accounts Authorized Local Users on the Operating System List the user accounts that are authorized locally on the operating system. This list includes both users requried by the operating system and by the installed applications. Depending on the Operating System distribution, version, software groups and applications, the user list is different and can be customized with scap-workbench. OVAL regular expression is used for the user list. The list starts with '^' and ends with '$' so that it matches exactly the username, not any string that includes the username. Users are separated with '|'. For example, three users: bin, oracle and sapadm are allowed, then the list is ^(bin|oracle|sapadm)$. The user root is the only user that is hard coded in OVAL that is always allowed on the operating system. ^(abrt|adm|avahi|bin|chrony|clevis|cockpit-ws|cockpit-wsinstance|colord|daemon|dbus|dnsmasq|flatpak|ftp|games|gdm|geoclue|gluster|gnome-initial-setup|halt|libstoragemgmt|lp|mail|nfsnobody|nobody|ntp|operator|oprofile|oracle|pcp|pegasus|pipewire|polkitd|postfix|pulse|qemu|radvd|rngd|root|rpc|rpcuser|rtkit|saned|saslauth|setroubleshoot|shutdown|sshd|sssd|sync|systemd-bus-proxy|systemd-coredump|systemd-network|systemd-resolve|tcpdump|tss|unbound|usbmuxd$|uuidd)$ ^(abrt|adm|avahi|bin|chrony|clevis|cockpit-ws|cockpit-wsinstance|colord|daemon|dbus|dnsmasq|flatpak|ftp|games|gdm|geoclue|gluster|gnome-initial-setup|halt|libstoragemgmt|lp|mail|nfsnobody|nobody|ntp|operator|oprofile|oracle|pcp|pegasus|pipewire|polkitd|postfix|pulse|qemu|radvd|rngd|root|rpc|rpcuser|rtkit|saned|saslauth|setroubleshoot|shutdown|sshd|sssd|sync|systemd-bus-proxy|systemd-coredump|systemd-network|systemd-resolve|tcpdump|tss|unbound|usbmuxd$|uuidd)$ ^(abrt|adm|avahi|bin|chrony|clevis|cockpit-ws|cockpit-wsinstance|colord|daemon|dbus|dnsmasq|fapolicyd|flatpak|ftp|games|gdm|geoclue|gluster|gnome-initial-setup|halt|libstoragemgmt|lp|mail|nfsnobody|nobody|ntp|operator|oprofile|oracle|pcp|pegasus|pipewire|polkitd|postfix|pulse|qemu|radvd|rngd|root|rpc|rpcuser|rtkit|saned|saslauth|setroubleshoot|shutdown|sshd|sssd|sync|systemd-bus-proxy|systemd-coredump|systemd-network|systemd-oom|systemd-resolve|tcpdump|tss|unbound|usbmuxd$|uuidd)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|pegasus|systemd-bus-proxy|systemd-network|dbus|polkitd|abrt|unbound|tss|libstoragemgmt|rpc|colord|usbmuxd$|pcp|saslauth|geoclue|setroubleshoot|rtkit|chrony|qemu|radvd|rpcuser|nfsnobody|pulse|gdm|gnome-initial-setup|postfix|avahi|ntp|sshd|tcpdump|oprofile|uuidd)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|pegasus|systemd-bus-proxy|systemd-network|dbus|polkitd|abrt|unbound|tss|libstoragemgmt|rpc|colord|usbmuxd$|pcp|saslauth|geoclue|setroubleshoot|rtkit|chrony|qemu|radvd|rpcuser|nfsnobody|pulse|gdm|gnome-initial-setup|postfix|avahi|ntp|sshd|tcpdump|oprofile|uuidd|systemd-resolve|systemd-coredump|sssd|rngd)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|tss|systemd-coredump|dbus|polkitd|avahi|colord|rtkit|pipewire|clevis|sssd|geoclue|flatpak|setroubleshoot|libstoragemgmt|systemd-oom|gdm|cockpit-ws|cockpit-wsinstance|gnome-initial-setup|sshd|chrony|dnsmasq|tcpdump|admin)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|pegasus|systemd-bus-proxy|systemd-network|dbus|polkitd|abrt|unbound|tss|libstoragemgmt|rpc|colord|usbmuxd$|pcp|saslauth|geoclue|setroubleshoot|rtkit|chrony|qemu|radvd|rpcuser|nfsnobody|pulse|gdm|gnome-initial-setup|postfix|avahi|ntp|sshd|tcpdump|oprofile|uuidd|systemd-resolve|systemd-coredump|sssd|rngd|man|systemd-timesync|scard|hacluster|statd|at|dockremap|vnc)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|pegasus|systemd-bus-proxy|systemd-network|dbus|polkitd|abrt|unbound|tss|libstoragemgmt|rpc|colord|usbmuxd$|pcp|saslauth|geoclue|setroubleshoot|rtkit|chrony|qemu|radvd|rpcuser|nfsnobody|pulse|gdm|gnome-initial-setup|postfix|avahi|ntp|sshd|tcpdump|oprofile|uuidd|systemd-resolve|systemd-coredump|sssd|rngd|man|systemd-timesync|scard|hacluster|statd|at|dockremap|vnc|messagebus|nscd|flatpak|srvGeoClue|tftp|wsdd|dnsmasq|usbmux|brltty)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|pegasus|systemd-bus-proxy|systemd-network|dbus|polkitd|abrt|unbound|tss|libstoragemgmt|rpc|colord|usbmuxd$|pcp|saslauth|geoclue|setroubleshoot|rtkit|chrony|qemu|radvd|rpcuser|nfsnobody|pulse|gdm|gnome-initial-setup|postfix|avahi|ntp|sshd|tcpdump|oprofile|uuidd|systemd-resolve|systemd-coredump|sssd|rngd|man|systemd-timesync|scard|hacluster|statd|at|dockremap|vnc|messagebus|nscd|flatpak|srvGeoClue|tftp|wsdd|dnsmasq|usbmux|brltty|salt|cockpit-ws|cockpit-wsinstance)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|pegasus|systemd-bus-proxy|systemd-network|dbus|polkitd|abrt|unbound|tss|libstoragemgmt|rpc|colord|usbmuxd$|pcp|saslauth|geoclue|setroubleshoot|rtkit|chrony|qemu|radvd|rpcuser|nfsnobody|pulse|gdm|gnome-initial-setup|postfix|avahi|ntp|sshd|tcpdump|oprofile|uuidd|systemd-resolve|systemd-coredump|sssd|rngd|man|systemd-timesync|scard|hacluster|statd|at|dockremap|vnc|messagebus|nscd|flatpak|srvGeoClue|tftp|wsdd|dnsmasq|usbmux|brltty|salt|cockpit-ws|cockpit-wsinstance)$ ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|tss|systemd-coredump|dbus|polkitd|avahi|colord|rtkit|pipewire|clevis|sssd|geoclue|flatpak|setroubleshoot|libstoragemgmt|systemd-oom|gdm|cockpit-ws|cockpit-wsinstance|gnome-initial-setup|sshd|chrony|dnsmasq|tcpdump|admin)$ Ensure All Accounts on the System Have Unique User IDs Change user IDs (UIDs), or delete accounts, so each has a unique name. Automatic remediation of this control is not available due to unique requirements of each system. Req-8.1.1 SRG-OS-000104-GPOS-00051 SRG-OS-000121-GPOS-00062 7.2.4 8.2.1 8.2 To assure accountability and prevent unauthenticated access, interactive users must be identified and authenticated to prevent potential misuse and compromise of the system. Only Authorized Local User Accounts Exist on Operating System Enterprise Application tends to use the server or virtual machine exclusively. Besides the default operating system user, there should be only authorized local users required by the installed software groups and applications that exist on the operating system. The authorized user list can be customized in the refine value variable var_accounts_authorized_local_users_regex. OVAL regular expression is used for the user list. Configure the system so all accounts on the system are assigned to an active system, application, or user account. Remove accounts that do not support approved system activities or that allow for a normal user to perform administrative-level actions. To remove unauthorized system accounts, use the following command: $ sudo userdel unauthorized_user SRG-OS-000480-GPOS-00227 Accounts providing no operational purpose provide additional opportunities for system compromise. Unnecessary accounts include user accounts for individuals not requiring access to the system and application accounts for applications not installed on the system. Ensure All Groups on the System Have Unique Group ID Change the group name or delete groups, so each has a unique id. Automatic remediation of this control is not available due to the unique requirements of each system. SRG-OS-000104-GPOS-00051 7.2.5 8.2.1 8.2 To assure accountability and prevent unauthenticated access, groups must be identified uniquely to prevent potential misuse and compromise of the system. Ensure All Groups on the System Have Unique Group Names Change the group name or delete groups, so each has a unique name. Automatic remediation of this control is not available due to the unique requirements of each system. 7.2.7 8.2.1 8.2 To assure accountability and prevent unauthenticated access, groups must be identified uniquely to prevent potential misuse and compromise of the system. Set Account Expiration Parameters Accounts can be configured to be automatically disabled after a certain time period, meaning that they will require administrator interaction to become usable again. Expiration of accounts after inactivity can be set for all accounts by default and also on a per-account basis, such as for accounts that are known to be temporary. To configure automatic expiration of an account following the expiration of its password (that is, after the password has expired and not been changed), run the following command, substituting NUM_DAYS and USER appropriately: $ sudo chage -I NUM_DAYS USER Accounts, such as temporary accounts, can also be configured to expire on an explicitly-set date with the -E option. The file /etc/default/useradd controls default settings for all newly-created accounts created with the system's normal command line utilities. This will only apply to newly created accounts number of days after a password expires until the account is permanently disabled The number of days to wait after a password expires, until the account will be permanently disabled. 0 180 30 35 40 45 60 90 35 Set Account Expiration Following Inactivity To specify the number of days after a password expires (which signifies inactivity) until an account is permanently disabled, add or correct the following line in /etc/default/useradd: INACTIVE= If a password is currently on the verge of expiration, then day(s) remain(s) until the account is automatically disabled. However, if the password will not expire for another 60 days, then 60 days plus day(s) could elapse until the account would be automatically disabled. See the useradd man page for more information. 1 12 13 14 15 16 18 3 5 7 8 5.6.2.1.1 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.6 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 A.12.4.1 A.12.4.3 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 IA-4(e) AC-2(3) CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 Req-8.1.4 SRG-OS-000118-GPOS-00060 5.4.1.5 8.2.6 8.2 Inactive identifiers pose a risk to systems and applications because attackers may exploit an inactive identifier and potentially obtain undetected access to the system. Disabling inactive accounts ensures that accounts which may not have been responsibly removed are not available to attackers who may have compromised their credentials. Owners of inactive accounts will not notice if unauthorized access to their user account has been obtained. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then var_account_disable_post_pw_expiration='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^INACTIVE") # shellcheck disable=SC2059 printf -v formatted_output "%s=%s" "$stripped_key" "$var_account_disable_post_pw_expiration" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^INACTIVE\\>" "/etc/default/useradd"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^INACTIVE\\>.*/$escaped_formatted_output/gi" "/etc/default/useradd" else if [[ -s "/etc/default/useradd" ]] && [[ -n "$(tail -c 1 -- "/etc/default/useradd" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/default/useradd" fi printf '%s\n' "$formatted_output" >> "/etc/default/useradd" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.6 - NIST-800-53-AC-2(3) - NIST-800-53-CM-6(a) - NIST-800-53-IA-4(e) - PCI-DSS-Req-8.1.4 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.6 - account_disable_post_pw_expiration - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_account_disable_post_pw_expiration # promote to variable set_fact: var_account_disable_post_pw_expiration: !!str tags: - always - name: Set Account Expiration Following Inactivity ansible.builtin.lineinfile: create: true dest: /etc/default/useradd regexp: ^INACTIVE line: INACTIVE={{ var_account_disable_post_pw_expiration }} when: '"shadow-utils" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.6 - NIST-800-53-AC-2(3) - NIST-800-53-CM-6(a) - NIST-800-53-IA-4(e) - PCI-DSS-Req-8.1.4 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.6 - account_disable_post_pw_expiration - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Assign Expiration Date to Emergency Accounts Emergency accounts are privileged accounts established in response to crisis situations where the need for rapid account activation is required. In the event emergency accounts are required, configure the system to terminate them after a documented time period. For every emergency account, run the following command to set an expiration date on it, substituting ACCOUNT_NAME and YYYY-MM-DD appropriately: $ sudo chage -E YYYY-MM-DD ACCOUNT_NAME YYYY-MM-DD indicates the documented expiration date for the account. For U.S. Government systems, the operating system must be configured to automatically terminate these types of accounts after a period of 72 hours. Due to the unique requirements of each system, automated remediation is not available for this configuration check. This rule is deprecated in favor of the account_temp_expire_date rule.Please consider replacing this rule in your files as it is not expected to receive updates as of version 0.1.69. 1 12 13 14 15 16 18 3 5 7 8 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS06.03 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 A.12.4.1 A.12.4.3 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 AC-2(2) AC-2(3) CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 SRG-OS-000123-GPOS-00064 SRG-OS-000002-GPOS-00002 If emergency user accounts remain active when no longer needed or for an excessive period, these accounts may be used to gain unauthorized access. To mitigate this risk, automated termination of all emergency accounts must be set upon account creation. Assign Expiration Date to Temporary Accounts Temporary accounts are established as part of normal account activation procedures when there is a need for short-term accounts. In the event temporary accounts are required, configure the system to terminate them after a documented time period. For every temporary account, run the following command to set an expiration date on it, substituting USER and YYYY-MM-DD appropriately: $ sudo chage -E YYYY-MM-DD USER YYYY-MM-DD indicates the documented expiration date for the account. For U.S. Government systems, the operating system must be configured to automatically terminate these types of accounts after a period of 72 hours. 1 12 13 14 15 16 18 3 5 7 8 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS06.03 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 A.12.4.1 A.12.4.3 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 AC-2(2) AC-2(3) CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 SRG-OS-000123-GPOS-00064 SRG-OS-000002-GPOS-00002 If temporary user accounts remain active when no longer needed or for an excessive period, these accounts may be used to gain unauthorized access. To mitigate this risk, automated termination of all temporary accounts must be set upon account creation. Ensure All Accounts on the System Have Unique Names Ensure accounts on the system have unique names. To ensure all accounts have unique names, run the following command: $ sudo getent passwd | awk -F: '{ print $1}' | uniq -d If a username is returned, change or delete the username. 5.5.2 Req-8.1.1 7.2.6 8.2.1 8.2 Unique usernames allow for accountability on the system. Use Centralized and Automated Authentication Implement an automated system for managing user accounts that minimizes the risk of errors, either intentional or deliberate. This system should integrate with an existing enterprise user management system, such as one based on Identity Management tools such as Active Directory, Kerberos, Directory Server, etc. A comprehensive account management process that includes automation helps to ensure the accounts designated as requiring attention are consistently and promptly addressed. Enterprise environments make user account management challenging and complex. A user management process requiring administrators to manually address account management functions adds risk of potential oversight. Set Password Expiration Parameters The file /etc/login.defs controls several password-related settings. Programs such as passwd, su, and login consult /etc/login.defs to determine behavior with regard to password aging, expiration warnings, and length. See the man page login.defs(5) for more information. Users should be forced to change their passwords, in order to decrease the utility of compromised passwords. However, the need to change passwords often should be balanced against the risk that users will reuse or write down passwords if forced to change them too often. Forcing password changes every 90-360 days, depending on the environment, is recommended. Set the appropriate value as PASS_MAX_DAYS and apply it to existing accounts with the -M flag. The PASS_MIN_DAYS (-m) setting prevents password changes for 7 days after the first change, to discourage password cycling. If you use this setting, train users to contact an administrator for an emergency password change in case a new password becomes compromised. The PASS_WARN_AGE (-W) setting gives users 7 days of warnings at login time that their passwords are about to expire. For example, for each existing human user USER, expiration parameters could be adjusted to a 180 day maximum password age, 7 day minimum password age, and 7 day warning period with the following command: $ sudo chage -M 180 -m 7 -W 7 USER maximum password age Maximum age of password in days 365 120 180 90 60 45 60 minimum password age Minimum age of password in days 0 1 2 3 4 5 6 7 7 minimum password length Minimum number of characters in password This will only check new passwords 10 12 14 15 17 18 20 6 8 15 warning days before password expires The number of days' warning given before a password expires. This will only apply to newly created accounts 0 14 10 7 7 Set Password Maximum Age To specify password maximum age for new accounts, edit the file /etc/login.defs and add or correct the following line: PASS_MAX_DAYS The profile requirement is . 1 12 15 16 5 5.6.2.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.6 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0418 1055 1402 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(f) IA-5(1)(d) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.4 SRG-OS-000076-GPOS-00044 5.4.1.1 8.3.9 8.3 Any password, no matter how complex, can eventually be cracked. Therefore, passwords need to be changed periodically. If the operating system does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the operating system passwords could be compromised. Setting the password maximum age ensures users are required to periodically change their passwords. Requiring shorter password lifetimes increases the risk of users writing down the password in a convenient location subject to physical compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then var_accounts_maximum_age_login_defs='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_MAX_DAYS") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_maximum_age_login_defs" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^PASS_MAX_DAYS\\>" "/etc/login.defs"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^PASS_MAX_DAYS\\>.*/$escaped_formatted_output/gi" "/etc/login.defs" else if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs" fi printf '%s\n' "$formatted_output" >> "/etc/login.defs" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1 - NIST-800-171-3.5.6 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.4 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.9 - accounts_maximum_age_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_maximum_age_login_defs # promote to variable set_fact: var_accounts_maximum_age_login_defs: !!str tags: - always - name: Set Password Maximum Age ansible.builtin.lineinfile: create: true dest: /etc/login.defs regexp: ^#?PASS_MAX_DAYS line: PASS_MAX_DAYS {{ var_accounts_maximum_age_login_defs }} when: '"shadow-utils" in ansible_facts.packages' tags: - CJIS-5.6.2.1 - NIST-800-171-3.5.6 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.4 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.9 - accounts_maximum_age_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Password Minimum Age To specify password minimum age for new accounts, edit the file /etc/login.defs and add or correct the following line: PASS_MIN_DAYS A value of 1 day is considered sufficient for many environments. The profile requirement is . 1 12 15 16 5 5.6.2.1.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.8 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0418 1055 1402 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(f) IA-5(1)(d) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000075-GPOS-00043 5.4.1.2 Enforcing a minimum password lifetime helps to prevent repeated password changes to defeat the password reuse or history enforcement requirement. If users are allowed to immediately and continually change their password, then the password could be repeatedly changed in a short period of time to defeat the organization's policy regarding password reuse. Setting the minimum password age protects against users cycling back to a favorite password after satisfying the password reuse requirement. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then var_accounts_minimum_age_login_defs='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_MIN_DAYS") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_minimum_age_login_defs" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^PASS_MIN_DAYS\\>" "/etc/login.defs"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^PASS_MIN_DAYS\\>.*/$escaped_formatted_output/gi" "/etc/login.defs" else if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs" fi printf '%s\n' "$formatted_output" >> "/etc/login.defs" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - accounts_minimum_age_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_minimum_age_login_defs # promote to variable set_fact: var_accounts_minimum_age_login_defs: !!str tags: - always - name: Set Password Minimum Age ansible.builtin.lineinfile: create: true dest: /etc/login.defs regexp: ^#?PASS_MIN_DAYS line: PASS_MIN_DAYS {{ var_accounts_minimum_age_login_defs }} when: '"shadow-utils" in ansible_facts.packages' tags: - CJIS-5.6.2.1.1 - NIST-800-171-3.5.8 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - accounts_minimum_age_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Password Minimum Length in login.defs To specify password length requirements for new accounts, edit the file /etc/login.defs and add or correct the following line: PASS_MIN_LEN The profile requirement is . If a program consults /etc/login.defs and also another PAM module (such as pam_pwquality) during a password change operation, then the most restrictive must be satisfied. See PAM section for more information about enforcing password quality requirements. 1 12 15 16 5 5.6.2.1 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.7 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(f) IA-5(1)(a) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000078-GPOS-00046 R31 Requiring a minimum password length makes password cracking attacks more difficult by ensuring a larger search space. However, any security benefit from an onerous requirement must be carefully weighed against usability problems, support costs, or counterproductive behavior that may result. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then var_accounts_password_minlen_login_defs='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_MIN_LEN") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_password_minlen_login_defs" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^PASS_MIN_LEN\\>" "/etc/login.defs"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^PASS_MIN_LEN\\>.*/$escaped_formatted_output/gi" "/etc/login.defs" else if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs" fi printf '%s\n' "$formatted_output" >> "/etc/login.defs" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.6.2.1 - NIST-800-171-3.5.7 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(f) - accounts_password_minlen_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_password_minlen_login_defs # promote to variable set_fact: var_accounts_password_minlen_login_defs: !!str tags: - always - name: Set Password Minimum Length in login.defs ansible.builtin.lineinfile: dest: /etc/login.defs regexp: ^PASS_MIN_LEN *[0-9]* state: present line: PASS_MIN_LEN {{ var_accounts_password_minlen_login_defs }} create: true when: '"shadow-utils" in ansible_facts.packages' tags: - CJIS-5.6.2.1 - NIST-800-171-3.5.7 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(f) - accounts_password_minlen_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Existing Passwords Maximum Age Configure non-compliant accounts to enforce a -day maximum password lifetime restriction by running the following command: $ sudo chage -M USER IA-5(f) IA-5(1)(d) CM-6(a) SRG-OS-000076-GPOS-00044 5.4.1.1 8.3.9 8.3 Any password, no matter how complex, can eventually be cracked. Therefore, passwords need to be changed periodically. If the operating system does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the operating system passwords could be compromised. var_accounts_maximum_age_login_defs='' while IFS= read -r i; do chage -M $var_accounts_maximum_age_login_defs $i done < <(awk -v var="$var_accounts_maximum_age_login_defs" -F: '(/^[^:]+:[^!*]/ && ($5 > var || $5 == "")) {print $1}' /etc/shadow) Set Existing Passwords Minimum Age Configure non-compliant accounts to enforce a 24 hours/1 day minimum password lifetime by running the following command: $ sudo chage -m 1 USER IA-5(f) IA-5(1)(d) CM-6(a) SRG-OS-000075-GPOS-00043 5.4.1.2 Enforcing a minimum password lifetime helps to prevent repeated password changes to defeat the password reuse or history enforcement requirement. If users are allowed to immediately and continually change their password, the password could be repeatedly changed in a short period of time to defeat the organization's policy regarding password reuse. var_accounts_minimum_age_login_defs='' while IFS= read -r i; do chage -m $var_accounts_minimum_age_login_defs $i done < <(awk -v var="$var_accounts_minimum_age_login_defs" -F: '(/^[^:]+:[^!*]/ && ($4 < var || $4 == "")) {print $1}' /etc/shadow) - name: XCCDF Value var_accounts_minimum_age_login_defs # promote to variable set_fact: var_accounts_minimum_age_login_defs: !!str tags: - always - name: Collect users with not correct minimum time period between password changes ansible.builtin.command: | awk -F':' '(/^[^:]+:[^!*]/ && ($4 < {{ var_accounts_minimum_age_login_defs }} || $4 == "")) {print $1}' /etc/shadow register: user_names tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - accounts_password_set_min_life_existing - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Change the minimum time period between password changes ansible.builtin.command: | chage -m {{ var_accounts_minimum_age_login_defs }} {{ item }} with_items: '{{ user_names.stdout_lines }}' when: user_names.stdout_lines | length > 0 tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - accounts_password_set_min_life_existing - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set Existing Passwords Warning Age To configure how many days prior to password expiration that a warning will be issued to users, run the command: $ sudo chage --warndays USER This profile requirement is . IA-5(f) IA-5(1)(d) CM-6(a) 5.4.1.3 8.3.9 8.3 Providing an advance warning that a password will be expiring gives users time to think of a secure password. Users caught unaware may choose a simple password or write it down where it may be discovered. var_accounts_password_warn_age_login_defs='' while IFS= read -r i; do chage --warndays $var_accounts_password_warn_age_login_defs $i done < <(awk -v var="$var_accounts_password_warn_age_login_defs" -F: '(($6 < var || $6 == "") && $2 ~ /^\$/) {print $1}' /etc/shadow) - name: XCCDF Value var_accounts_password_warn_age_login_defs # promote to variable set_fact: var_accounts_password_warn_age_login_defs: !!str tags: - always - name: Set Existing Passwords Warning Age - Collect Users With Incorrect Number of Days of Warning Before Password Expires ansible.builtin.command: cmd: awk -F':' '(($6 < {{ var_accounts_password_warn_age_login_defs }} || $6 == "") && $2 ~ /^\$/) {print $1}' /etc/shadow register: result_pass_warn_age_user_names changed_when: false tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.9 - accounts_password_set_warn_age_existing - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set Existing Passwords Warning Age - Ensure the Number of Days of Warning Before Password Expires ansible.builtin.command: cmd: chage --warndays {{ var_accounts_password_warn_age_login_defs }} {{ item }} with_items: '{{ result_pass_warn_age_user_names.stdout_lines }}' when: result_pass_warn_age_user_names is not skipped and result_pass_warn_age_user_names.stdout_lines | length > 0 tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.9 - accounts_password_set_warn_age_existing - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Set Password Warning Age To specify how many days prior to password expiration that a warning will be issued to users, edit the file /etc/login.defs and add or correct the following line: PASS_WARN_AGE The profile requirement is . 1 12 13 14 15 16 18 3 5 7 8 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.8 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 0418 1055 1402 A.12.4.1 A.12.4.3 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 IA-5(f) IA-5(1)(d) CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 Req-8.2.4 5.4.1.3 8.3.9 8.3 Setting the password warning age enables users to make the change at a practical time. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then var_accounts_password_warn_age_login_defs='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_WARN_AGE") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_password_warn_age_login_defs" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^PASS_WARN_AGE\\>" "/etc/login.defs"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^PASS_WARN_AGE\\>.*/$escaped_formatted_output/gi" "/etc/login.defs" else if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs" fi printf '%s\n' "$formatted_output" >> "/etc/login.defs" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.5.8 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.4 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.9 - accounts_password_warn_age_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_password_warn_age_login_defs # promote to variable set_fact: var_accounts_password_warn_age_login_defs: !!str tags: - always - name: Set Password Warning Age ansible.builtin.lineinfile: dest: /etc/login.defs regexp: ^PASS_WARN_AGE *[0-9]* state: present line: PASS_WARN_AGE {{ var_accounts_password_warn_age_login_defs }} create: true when: '"shadow-utils" in ansible_facts.packages' tags: - NIST-800-171-3.5.8 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(d) - NIST-800-53-IA-5(f) - PCI-DSS-Req-8.2.4 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.9 - accounts_password_warn_age_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set existing passwords a period of inactivity before they been locked Configure user accounts that have been inactive for over a given period of time to be automatically disabled by running the following command: $ sudo chage --inactive 30 USER DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.6 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 A.12.4.1 A.12.4.3 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 IA-4(e) AC-2(3) CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 Req-8.1.4 SRG-OS-000118-GPOS-00060 5.4.1.5 8.2.6 8.2 Inactive accounts pose a threat to system security since the users are not logging in to notice failed login attempts or other anomalies. var_account_disable_post_pw_expiration='' while IFS= read -r i; do chage --inactive $var_account_disable_post_pw_expiration $i done < <(awk -v var="$var_account_disable_post_pw_expiration" -F: '(($7 > var || $7 == "") && $2 ~ /^\$/) {print $1}' /etc/shadow) - name: XCCDF Value var_account_disable_post_pw_expiration # promote to variable set_fact: var_account_disable_post_pw_expiration: !!str tags: - always - name: Collect users with not correct INACTIVE parameter set ansible.builtin.command: cmd: awk -F':' '(($7 > {{ var_account_disable_post_pw_expiration }} || $7 == "") && $2 ~ /^\$/) {print $1}' /etc/shadow register: user_names changed_when: false tags: - NIST-800-171-3.5.6 - NIST-800-53-AC-2(3) - NIST-800-53-CM-6(a) - NIST-800-53-IA-4(e) - PCI-DSS-Req-8.1.4 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.6 - accounts_set_post_pw_existing - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Change the period of inactivity ansible.builtin.command: cmd: chage --inactive {{ var_account_disable_post_pw_expiration }} {{ item }} with_items: '{{ user_names.stdout_lines }}' when: user_names is not skipped and user_names.stdout_lines | length > 0 tags: - NIST-800-171-3.5.6 - NIST-800-53-AC-2(3) - NIST-800-53-CM-6(a) - NIST-800-53-IA-4(e) - PCI-DSS-Req-8.1.4 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.6 - accounts_set_post_pw_existing - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Verify Proper Storage and Existence of Password Hashes By default, password hashes for local accounts are stored in the second field (colon-separated) in /etc/shadow. This file should be readable only by processes running with root credentials, preventing users from casually accessing others' password hashes and attempting to crack them. However, it remains possible to misconfigure the system and store password hashes in world-readable files such as /etc/passwd, or to even store passwords themselves in plaintext on the system. Using system-provided tools for password change/creation should allow administrators to avoid such misconfiguration. Password Hashing algorithm Specify the number of rounds for the system password encryption algorithm. Defines the value set in /etc/pam.d/system-auth and /etc/pam.d/password-auth 5000 5000 65536 100000 11 5 Verify All Account Password Hashes are Shadowed If any password hashes are stored in /etc/passwd (in the second field, instead of an x or *), the cause of this misconfiguration should be investigated. The account should have its password reset and the hash should be properly stored, or the account should be deleted entirely. 1 12 15 16 5 5.5.2 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.5.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 1410 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(h) CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.2.1 7.2.1 8.3.2 8.3 The hashes for all user account passwords should be stored in the file /etc/shadow and never in /etc/passwd, which is readable by all users. Verify All Account Password Hashes are Shadowed with SHA512 Verify the operating system requires the shadow password suite configuration be set to encrypt interactive user passwords using a strong cryptographic hash. Check that the interactive user account passwords are using a strong password hash with the following command: $ sudo cut -d: -f2 /etc/shadow $6$kcOnRq/5$NUEYPuyL.wghQwWssXRcLRFiiru7f5JPV6GaJhNC2aK5F3PZpE/BCCtwrxRc/AInKMNX3CdMw11m9STiql12f/ Password hashes ! or * indicate inactive accounts not available for logon and are not evaluated. If any interactive user password hash does not begin with $6, this is a finding. IA-5(1)(c) IA-5(1).1(v) IA-7 IA-7.1 SRG-OS-000073-GPOS-00041 SRG-OS-000120-GPOS-00061 Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Ensure all users last password change date is in the past All users should have a password change date in the past. Automatic remediation is not available, in order to avoid any system disruption. 5.4.1.6 8.3.5 8.3 If a user recorded password change date is in the future then they could bypass any set password expiration. Set number of Password Hashing Rounds - password-auth Configure the number or rounds for the password hashing algorithm. This can be accomplished by using the rounds option for the pam_unix PAM module. In file /etc/pam.d/password-auth append rounds= to the pam_unix.so entry, as shown below: password sufficient pam_unix.so ...existing_options... rounds= The system's default number of rounds is 5000. Setting a high number of hashing rounds makes it more difficult to brute force the password, but requires more CPU resources to authenticate users. SRG-OS-000073-GPOS-00041 R68 Using a higher number of rounds makes password cracking attacks more difficult. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_password_pam_unix_rounds='' if [ -e "/etc/pam.d/password-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/password-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_unix.so.*)/\1sufficient \2/" "$PAM_FILE_PATH" else echo "password sufficient pam_unix.so" >> "$PAM_FILE_PATH" fi fi # Check the option if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*\srounds\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+sufficient\s+pam_unix.so.*/ s/$/ rounds=$var_password_pam_unix_rounds/" "$PAM_FILE_PATH" else sed -i -E --follow-symlinks "s/(\s*password\s+sufficient\s+pam_unix.so\s+.*)(rounds=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_unix_rounds \3/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/password-auth was not found" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - accounts_password_pam_unix_rounds_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_password_pam_unix_rounds # promote to variable set_fact: var_password_pam_unix_rounds: !!str tags: - always - name: Set number of Password Hashing Rounds - password-auth - Check if /etc/pam.d/password-auth file is present ansible.builtin.stat: path: /etc/pam.d/password-auth register: result_pam_file_present when: '"pam" in ansible_facts.packages' tags: - accounts_password_pam_unix_rounds_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Set number of Password Hashing Rounds - password-auth - Check the proper remediation for the system block: - name: Set number of Password Hashing Rounds - password-auth - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/password-auth - name: Set number of Password Hashing Rounds - password-auth - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect custom profile is used if authselect is present block: - name: Set number of Password Hashing Rounds - password-auth - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set number of Password Hashing Rounds - password-auth - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set number of Password Hashing Rounds - password-auth - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set number of Password Hashing Rounds - password-auth - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set number of Password Hashing Rounds - password-auth - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set number of Password Hashing Rounds - password-auth - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set number of Password Hashing Rounds - password-auth - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set number of Password Hashing Rounds - password-auth - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set number of Password Hashing Rounds - password-auth - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set number of Password Hashing Rounds - password-auth - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set number of Password Hashing Rounds - password-auth - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set number of Password Hashing Rounds - password-auth - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set number of Password Hashing Rounds - password-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set number of Password Hashing Rounds - password-auth - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Set number of Password Hashing Rounds - password-auth - Include or update the PAM module line in {{ pam_file_path }} block: - name: Set number of Password Hashing Rounds - password-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_unix.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Set number of Password Hashing Rounds - password-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_unix.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Set number of Password Hashing Rounds - password-auth - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' line: password {{ pam_module_control }} pam_unix.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Set number of Password Hashing Rounds - password-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set number of Password Hashing Rounds - password-auth - Check if the required PAM module option is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*\srounds\b state: absent check_mode: true changed_when: false register: result_pam_module_accounts_password_pam_unix_rounds_password_auth_option_present - name: Set number of Password Hashing Rounds - password-auth - Ensure the "rounds" PAM option for "pam_unix.so" is included in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so.*) line: \1 rounds={{ var_password_pam_unix_rounds }} state: present register: result_pam_accounts_password_pam_unix_rounds_password_auth_add when: - result_pam_module_accounts_password_pam_unix_rounds_password_auth_option_present.found is defined - result_pam_module_accounts_password_pam_unix_rounds_password_auth_option_present.found == 0 - name: Set number of Password Hashing Rounds - password-auth - Ensure the required value for "rounds" PAM option from "pam_unix.so" in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s+.*)(rounds)=[0-9a-zA-Z]*\s*(.*) line: \1\2={{ var_password_pam_unix_rounds }} \3 register: result_pam_accounts_password_pam_unix_rounds_password_auth_edit when: - result_pam_module_accounts_password_pam_unix_rounds_password_auth_option_present.found > 0 - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - |- (result_pam_accounts_password_pam_unix_rounds_password_auth_add is defined and result_pam_accounts_password_pam_unix_rounds_password_auth_add.changed) or (result_pam_accounts_password_pam_unix_rounds_password_auth_edit is defined and result_pam_accounts_password_pam_unix_rounds_password_auth_edit.changed) when: - '"pam" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - accounts_password_pam_unix_rounds_password_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed Set number of Password Hashing Rounds - system-auth Configure the number or rounds for the password hashing algorithm. This can be accomplished by using the rounds option for the pam_unix PAM module. In file /etc/pam.d/system-auth append rounds= to the pam_unix.so entry, as shown below: password sufficient pam_unix.so ...existing_options... rounds= The system's default number of rounds is 5000. Setting a high number of hashing rounds makes it more difficult to brute force the password, but requires more CPU resources to authenticate users. SRG-OS-000073-GPOS-00041 R68 Using a higher number of rounds makes password cracking attacks more difficult. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then var_password_pam_unix_rounds='' if [ -e "/etc/pam.d/system-auth" ] ; then PAM_FILE_PATH="/etc/pam.d/system-auth" if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }') # If not already in use, a custom profile is created preserving the enabled features. if [[ ! $CURRENT_PROFILE == custom/* ]]; then ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }') # The "local" profile does not contain essential security features required by multiple Benchmarks. # If currently used, it is replaced by "sssd", which is the best option in this case. if [[ $CURRENT_PROFILE == local ]]; then CURRENT_PROFILE="sssd" fi authselect create-profile hardening -b $CURRENT_PROFILE CURRENT_PROFILE="custom/hardening" authselect apply-changes -b --backup=before-hardening-custom-profile authselect select $CURRENT_PROFILE for feature in $ENABLED_FEATURES; do authselect enable-feature $feature; done authselect apply-changes -b --backup=after-hardening-custom-profile fi PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth") PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME" authselect apply-changes -b fi if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*" "$PAM_FILE_PATH"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_unix.so.*)/\1sufficient \2/" "$PAM_FILE_PATH" else echo "password sufficient pam_unix.so" >> "$PAM_FILE_PATH" fi fi # Check the option if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*\srounds\b" "$PAM_FILE_PATH"; then sed -i -E --follow-symlinks "/\s*password\s+sufficient\s+pam_unix.so.*/ s/$/ rounds=$var_password_pam_unix_rounds/" "$PAM_FILE_PATH" else sed -i -E --follow-symlinks "s/(\s*password\s+sufficient\s+pam_unix.so\s+.*)(rounds=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_unix_rounds \3/" "$PAM_FILE_PATH" fi if [ -f /usr/bin/authselect ]; then authselect apply-changes -b fi else echo "/etc/pam.d/system-auth was not found" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - accounts_password_pam_unix_rounds_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_password_pam_unix_rounds # promote to variable set_fact: var_password_pam_unix_rounds: !!str tags: - always - name: Set number of Password Hashing Rounds - system-auth - Check if /etc/pam.d/system-auth file is present ansible.builtin.stat: path: /etc/pam.d/system-auth register: result_pam_file_present when: '"pam" in ansible_facts.packages' tags: - accounts_password_pam_unix_rounds_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - name: Set number of Password Hashing Rounds - system-auth - Check the proper remediation for the system block: - name: Set number of Password Hashing Rounds - system-auth - Define the PAM file to be edited as a local fact ansible.builtin.set_fact: pam_file_path: /etc/pam.d/system-auth - name: Set number of Password Hashing Rounds - system-auth - Check if system relies on authselect tool ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect custom profile is used if authselect is present block: - name: Set number of Password Hashing Rounds - system-auth - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Set number of Password Hashing Rounds - system-auth - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Set number of Password Hashing Rounds - system-auth - Get authselect current profile ansible.builtin.shell: cmd: authselect current -r | awk '{ print $1 }' register: result_authselect_profile changed_when: false when: - result_authselect_check_cmd is success - name: Set number of Password Hashing Rounds - system-auth - Define the current authselect profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: '{{ result_authselect_profile.stdout }}' when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is match("custom/") - name: Set number of Password Hashing Rounds - system-auth - Define the new authselect custom profile as a local fact ansible.builtin.set_fact: authselect_current_profile: '{{ result_authselect_profile.stdout }}' authselect_custom_profile: custom/hardening when: - result_authselect_profile is not skipped - result_authselect_profile.stdout is not match("custom/") - name: Set number of Password Hashing Rounds - system-auth - Get authselect current features to also enable them in the custom profile ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set number of Password Hashing Rounds - system-auth - Check if any custom profile with the same name was already created ansible.builtin.stat: path: /etc/authselect/{{ authselect_custom_profile }} register: result_authselect_custom_profile_present changed_when: false when: - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - name: Set number of Password Hashing Rounds - system-auth - Create an authselect custom profile based on the current profile ansible.builtin.command: cmd: authselect create-profile hardening -b {{ authselect_current_profile }} when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is not match("^(custom/|local)") - not result_authselect_custom_profile_present.stat.exists - name: Set number of Password Hashing Rounds - system-auth - Create an authselect custom profile based on sssd profile ansible.builtin.command: cmd: authselect create-profile hardening -b sssd when: - result_authselect_profile is not skipped - result_authselect_check_cmd is success - authselect_current_profile is match("local") - not result_authselect_custom_profile_present.stat.exists - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=before-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set number of Password Hashing Rounds - system-auth - Ensure the authselect custom profile is selected ansible.builtin.command: cmd: authselect select {{ authselect_custom_profile }} register: result_pam_authselect_select_profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - authselect_current_profile is not match("custom/") - authselect_custom_profile is not match(authselect_current_profile) - name: Set number of Password Hashing Rounds - system-auth - Restore the authselect features in the custom profile ansible.builtin.command: cmd: authselect enable-feature {{ item }} loop: '{{ result_authselect_features.stdout_lines }}' register: result_pam_authselect_restore_features when: - result_authselect_profile is not skipped - result_authselect_features is not skipped - result_pam_authselect_select_profile is not skipped - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b --backup=after-hardening-custom-profile when: - result_authselect_check_cmd is success - result_authselect_profile is not skipped - result_pam_authselect_restore_features is not skipped - name: Set number of Password Hashing Rounds - system-auth - Change the PAM file to be edited according to the custom authselect profile ansible.builtin.set_fact: pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path | basename }} when: - authselect_custom_profile is defined when: - result_authselect_present.stat.exists - name: Set number of Password Hashing Rounds - system-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set number of Password Hashing Rounds - system-auth - Check if expected PAM module line is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Set number of Password Hashing Rounds - system-auth - Include or update the PAM module line in {{ pam_file_path }} block: - name: Set number of Password Hashing Rounds - system-auth - Check if required PAM module line is present in {{ pam_file_path }} with different control ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+.*\s+pam_unix.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Set number of Password Hashing Rounds - system-auth - Ensure the correct control for the required PAM module line in {{ pam_file_path }} ansible.builtin.replace: dest: '{{ pam_file_path }}' regexp: ^(\s*password\s+).*(\bpam_unix.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Set number of Password Hashing Rounds - system-auth - Ensure the required PAM module line is included in {{ pam_file_path }} ansible.builtin.lineinfile: dest: '{{ pam_file_path }}' line: password {{ pam_module_control }} pam_unix.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Set number of Password Hashing Rounds - system-auth - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Set number of Password Hashing Rounds - system-auth - Check if the required PAM module option is present in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*\srounds\b state: absent check_mode: true changed_when: false register: result_pam_module_accounts_password_pam_unix_rounds_system_auth_option_present - name: Set number of Password Hashing Rounds - system-auth - Ensure the "rounds" PAM option for "pam_unix.so" is included in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so.*) line: \1 rounds={{ var_password_pam_unix_rounds }} state: present register: result_pam_accounts_password_pam_unix_rounds_system_auth_add when: - result_pam_module_accounts_password_pam_unix_rounds_system_auth_option_present.found is defined - result_pam_module_accounts_password_pam_unix_rounds_system_auth_option_present.found == 0 - name: Set number of Password Hashing Rounds - system-auth - Ensure the required value for "rounds" PAM option from "pam_unix.so" in {{ pam_file_path }} ansible.builtin.lineinfile: path: '{{ pam_file_path }}' backrefs: true regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s+.*)(rounds)=[0-9a-zA-Z]*\s*(.*) line: \1\2={{ var_password_pam_unix_rounds }} \3 register: result_pam_accounts_password_pam_unix_rounds_system_auth_edit when: - result_pam_module_accounts_password_pam_unix_rounds_system_auth_option_present.found > 0 - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present.stat.exists - |- (result_pam_accounts_password_pam_unix_rounds_system_auth_add is defined and result_pam_accounts_password_pam_unix_rounds_system_auth_add.changed) or (result_pam_accounts_password_pam_unix_rounds_system_auth_edit is defined and result_pam_accounts_password_pam_unix_rounds_system_auth_edit.changed) when: - '"pam" in ansible_facts.packages' - result_pam_file_present.stat.exists tags: - accounts_password_pam_unix_rounds_system_auth - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed All GIDs referenced in /etc/passwd must be defined in /etc/group Add a group to the system for each GID referenced without a corresponding group. 1 12 15 16 5 5.5.2 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 IA-2 CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 Req-8.5.a SRG-OS-000104-GPOS-00051 7.2.3 8.2.2 8.2 If a user is assigned the Group Identifier (GID) of a group not existing on the system, and a group with the Group Identifier (GID) is subsequently created, the user may have unintended rights to any files associated with the group. Prevent Login to Accounts With Empty Password If an account is configured for password authentication but does not have an assigned password, it may be possible to log into the account without authentication. Remove any instances of the nullok in /etc/pam.d/system-auth and /etc/pam.d/password-auth to prevent logins with empty passwords. If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. Note that this rule is not applicable for systems running within a container. Having user with empty password within a container is not considered a risk, because it should not be possible to directly login into a container anyway. 1 12 13 14 15 16 18 3 5 5.5.2 APO01.06 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.02 DSS06.03 DSS06.10 3.1.1 3.1.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.18.1.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 IA-5(1)(a) IA-5(c) CM-6(a) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.DS-5 FIA_UAU.1 Req-8.2.3 SRG-OS-000480-GPOS-00227 5.3.3.4.1 8.3.1 8.3 If an account has an empty password, anyone could log in and run commands with the privileges of that account. Accounts with empty passwords should never be used in operational environments. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature without-nullok authselect apply-changes -b else if grep -qP "^\s*auth\s+sufficient\s+pam_unix.so\s.*\bnullok\b" "/etc/pam.d/system-auth"; then sed -i -E --follow-symlinks "s/(.*auth.*sufficient.*pam_unix.so.*)\bnullok\b=?[[:alnum:]]*(.*)/\1\2/g" "/etc/pam.d/system-auth" fi if grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s.*\bnullok\b" "/etc/pam.d/system-auth"; then sed -i -E --follow-symlinks "s/(.*password.*sufficient.*pam_unix.so.*)\bnullok\b=?[[:alnum:]]*(.*)/\1\2/g" "/etc/pam.d/system-auth" fi if grep -qP "^\s*auth\s+sufficient\s+pam_unix.so\s.*\bnullok\b" "/etc/pam.d/password-auth"; then sed -i -E --follow-symlinks "s/(.*auth.*sufficient.*pam_unix.so.*)\bnullok\b=?[[:alnum:]]*(.*)/\1\2/g" "/etc/pam.d/password-auth" fi if grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s.*\bnullok\b" "/etc/pam.d/password-auth"; then sed -i -E --follow-symlinks "s/(.*password.*sufficient.*pam_unix.so.*)\bnullok\b=?[[:alnum:]]*(.*)/\1\2/g" "/etc/pam.d/password-auth" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - configure_strategy - high_severity - low_complexity - medium_disruption - no_empty_passwords - no_reboot_needed - name: Prevent Login to Accounts With Empty Password - Check if system relies on authselect ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.2 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - configure_strategy - high_severity - low_complexity - medium_disruption - no_empty_passwords - no_reboot_needed - name: Prevent Login to Accounts With Empty Password - Remediate using authselect block: - name: Prevent Login to Accounts With Empty Password - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Prevent Login to Accounts With Empty Password - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Prevent Login to Accounts With Empty Password - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Prevent Login to Accounts With Empty Password - Ensure "without-nullok" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature without-nullok register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("without-nullok") - name: Prevent Login to Accounts With Empty Password - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"kernel" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - CJIS-5.5.2 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - configure_strategy - high_severity - low_complexity - medium_disruption - no_empty_passwords - no_reboot_needed - name: Prevent Login to Accounts With Empty Password - Remediate directly editing PAM files ansible.builtin.replace: dest: '{{ item }}' regexp: nullok loop: - /etc/pam.d/system-auth - /etc/pam.d/password-auth when: - '"kernel" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - CJIS-5.5.2 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(a) - NIST-800-53-IA-5(c) - PCI-DSS-Req-8.2.3 - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - configure_strategy - high_severity - low_complexity - medium_disruption - no_empty_passwords - no_reboot_needed --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,%23%20Generated%20by%20authselect%20on%20Sat%20Oct%2027%2014%3A59%3A36%202018%0A%23%20Do%20not%20modify%20this%20file%20manually.%0A%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_env.so%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_faildelay.so%20delay%3D2000000%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_fprintd.so%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20try_first_pass%0Aauth%20%20%20%20%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet_success%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20forward_pass%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3C%201000%20quiet%0Aaccount%20%20%20%20%20%5Bdefault%3Dbad%20success%3Dok%20user_unknown%3Dignore%5D%20pam_sss.so%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_permit.so%0A%0Apassword%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_pwquality.so%20try_first_pass%20local_users_only%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20sha512%20shadow%20try_first_pass%20use_authtok%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20use_authtok%0Apassword%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_keyinit.so%20revoke%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_limits.so%0A-session%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_systemd.so%0Asession%20%20%20%20%20%5Bsuccess%3D1%20default%3Dignore%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20service%20in%20crond%20quiet%20use_uid%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%0A mode: 0644 path: /etc/pam.d/password-auth overwrite: true - contents: source: data:,%23%20Generated%20by%20authselect%20on%20Sat%20Oct%2027%2014%3A59%3A36%202018%0A%23%20Do%20not%20modify%20this%20file%20manually.%0A%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_env.so%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_faildelay.so%20delay%3D2000000%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_fprintd.so%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20try_first_pass%0Aauth%20%20%20%20%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet_success%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20forward_pass%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3C%201000%20quiet%0Aaccount%20%20%20%20%20%5Bdefault%3Dbad%20success%3Dok%20user_unknown%3Dignore%5D%20pam_sss.so%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_permit.so%0A%0Apassword%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_pwquality.so%20try_first_pass%20local_users_only%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20sha512%20shadow%20try_first_pass%20use_authtok%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20use_authtok%0Apassword%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_keyinit.so%20revoke%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_limits.so%0A-session%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_systemd.so%0Asession%20%20%20%20%20%5Bsuccess%3D1%20default%3Dignore%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20service%20in%20crond%20quiet%20use_uid%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%0A mode: 0644 path: /etc/pam.d/system-auth overwrite: true Ensure There Are No Accounts With Blank or Null Passwords Check the "/etc/shadow" file for blank passwords with the following command: $ sudo awk -F: '!$2 {print $1}' /etc/shadow If the command returns any results, this is a finding. Configure all accounts on the system to have a password or lock the account with the following commands: Perform a password reset: $ sudo passwd [username] Lock an account: $ sudo passwd -l [username] Note that this rule is not applicable for systems running within a container. Having user with empty password within a container is not considered a risk, because it should not be possible to directly login into a container anyway. CM-6(b) CM-6.1(iv) SRG-OS-000480-GPOS-00227 7.2.2 2.2.2 2.2 If an account has an empty password, anyone could log in and run commands with the privileges of that account. Accounts with empty passwords should never be used in operational environments. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then readarray -t users_with_empty_pass < <(sudo awk -F: '!$2 {print $1}' /etc/shadow) for user_with_empty_pass in "${users_with_empty_pass[@]}" do passwd -l $user_with_empty_pass done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.2 - high_severity - low_complexity - low_disruption - no_empty_passwords_etc_shadow - no_reboot_needed - restrict_strategy - name: Collect users with no password ansible.builtin.command: | awk -F: '!$2 {print $1}' /etc/shadow register: users_nopasswd changed_when: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.2 - high_severity - low_complexity - low_disruption - no_empty_passwords_etc_shadow - no_reboot_needed - restrict_strategy - name: Lock users with no password ansible.builtin.command: | passwd -l {{ item }} with_items: '{{ users_nopasswd.stdout_lines }}' when: - '"kernel" in ansible_facts.packages' - users_nopasswd is not skipped and users_nopasswd.stdout_lines | length > 0 tags: - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.2 - high_severity - low_complexity - low_disruption - no_empty_passwords_etc_shadow - no_reboot_needed - restrict_strategy Verify No .forward Files Exist The .forward file specifies an email address to forward the user's mail to. 7.2.9 Use of the .forward file poses a security risk in that sensitive data may be inadvertently transferred outside the organization. The .forward file also poses a risk as it can be used to execute commands that may perform unintended actions. Ensure there are no legacy + NIS entries in /etc/group The + character in /etc/group file marks a place where entries from a network information service (NIS) should be directly inserted. Using this method to include entries into /etc/group is considered legacy and should be avoided. These entries may provide a way for an attacker to gain access to the system. if grep -q '^\+' /etc/group; then # backup old file to /etc/group- cp /etc/group /etc/group- sed -i '/^\+.*$/d' /etc/group fi - name: Ensure there are no legacy + NIS entries in /etc/group - Backup the Old /etc/group File ansible.builtin.copy: src: /etc/group dest: /etc/group- remote_src: true tags: - low_complexity - medium_disruption - medium_severity - no_legacy_plus_entries_etc_group - no_reboot_needed - restrict_strategy - name: Ensure there are no legacy + NIS entries in /etc/group - Remove Lines Starting with + From /etc/group ansible.builtin.lineinfile: regexp: ^\+.*$ state: absent path: /etc/group tags: - low_complexity - medium_disruption - medium_severity - no_legacy_plus_entries_etc_group - no_reboot_needed - restrict_strategy Ensure there are no legacy + NIS entries in /etc/passwd The + character in /etc/passwd file marks a place where entries from a network information service (NIS) should be directly inserted. Using this method to include entries into /etc/passwd is considered legacy and should be avoided. These entries may provide a way for an attacker to gain access to the system. if grep -q '^\+' /etc/passwd; then # backup old file to /etc/passwd- cp /etc/passwd /etc/passwd- sed -i '/^\+.*$/d' /etc/passwd fi - name: Ensure there are no legacy + NIS entries in /etc/passwd - Backup the Old /etc/passwd File ansible.builtin.copy: src: /etc/passwd dest: /etc/passwd- remote_src: true tags: - low_complexity - medium_disruption - medium_severity - no_legacy_plus_entries_etc_passwd - no_reboot_needed - restrict_strategy - name: Ensure there are no legacy + NIS entries in /etc/passwd - Remove Lines Starting with + From /etc/passwd ansible.builtin.lineinfile: regexp: ^\+.*$ state: absent path: /etc/passwd tags: - low_complexity - medium_disruption - medium_severity - no_legacy_plus_entries_etc_passwd - no_reboot_needed - restrict_strategy Ensure there are no legacy + NIS entries in /etc/shadow The + character in /etc/shadow file marks a place where entries from a network information service (NIS) should be directly inserted. Using this method to include entries into /etc/shadow is considered legacy and should be avoided. These entries may provide a way for an attacker to gain access to the system. if grep -q '^\+' /etc/shadow; then # backup old file to /etc/shadow- cp /etc/shadow /etc/shadow- sed -i '/^\+.*$/d' /etc/shadow fi - name: Ensure there are no legacy + NIS entries in /etc/shadow - Backup the Old /etc/shadow File ansible.builtin.copy: src: /etc/shadow dest: /etc/shadow- remote_src: true tags: - low_complexity - medium_disruption - medium_severity - no_legacy_plus_entries_etc_shadow - no_reboot_needed - restrict_strategy - name: Ensure there are no legacy + NIS entries in /etc/shadow - Remove Lines Starting with + From /etc/shadow ansible.builtin.lineinfile: regexp: ^\+.*$ state: absent path: /etc/shadow tags: - low_complexity - medium_disruption - medium_severity - no_legacy_plus_entries_etc_shadow - no_reboot_needed - restrict_strategy Verify No netrc Files Exist The .netrc files contain login information used to auto-login into FTP servers and reside in the user's home directory. These files may contain unencrypted passwords to remote FTP servers making them susceptible to access by unauthorized users and should not be used. Any .netrc files should be removed. 1 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.06 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-003-8 R1.3 CIP-003-8 R3 CIP-003-8 R3.1 CIP-003-8 R3.2 CIP-003-8 R3.3 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 IA-5(h) IA-5(1)(c) CM-6(a) IA-5(7) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.PT-3 7.2.9 Unencrypted passwords for remote FTP servers may be stored in .netrc files. Restrict Root Logins Direct root logins should be allowed only for emergency use. In normal situations, the administrator should access the system via a unique unprivileged account, and then use su or sudo to execute privileged commands. Discouraging administrators from accessing the root account directly ensures an audit trail in organizations with multiple administrators. Locking down the channels through which root can connect directly also reduces opportunities for password-guessing against the root account. The login program uses the file /etc/securetty to determine which interfaces should allow root logins. The virtual devices /dev/console and /dev/tty* represent the system consoles (accessible via the Ctrl-Alt-F1 through Ctrl-Alt-F6 keyboard sequences on a default installation). The default securetty file also contains /dev/vc/*. These are likely to be deprecated in most environments, but may be retained for compatibility. Root should also be prohibited from connecting via network protocols. Other sections of this document include guidance describing how to prevent root from logging in via SSH. Group Name Used by pam_wheel Group Parameter pam_wheel module has a parameter called group, which controls which groups can access the su command. This variable holds the valid value for the parameter. sugroup sugroup Verify Only Root Has UID 0 If any account other than root has a UID of 0, this misconfiguration should be investigated and the accounts other than root should be removed or have their UID changed. If the account is associated with system commands or applications the UID should be changed to one greater than "0" but less than "1000." Otherwise assign a UID greater than "1000" that has not already been assigned. 1 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.02 DSS06.03 DSS06.10 3.1.1 3.1.5 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.18.1.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 IA-2 AC-6(5) IA-4(b) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.DS-5 Req-8.5 SRG-OS-000480-GPOS-00227 5.4.2.1 8.2.1 8.2 An account has root authority if it has a UID of 0. Multiple accounts with a UID of 0 afford more opportunity for potential intruders to guess a password for a privileged account. Proper configuration of sudo is recommended to afford multiple system administrators access to root privileges in an accountable manner. awk -F: '$3 == 0 && $1 != "root" { print $1 }' /etc/passwd | xargs --no-run-if-empty --max-lines=1 passwd -l - name: Get all /etc/passwd file entries ansible.builtin.getent: database: passwd split: ':' tags: - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-6(5) - NIST-800-53-IA-2 - NIST-800-53-IA-4(b) - PCI-DSS-Req-8.5 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.1 - accounts_no_uid_except_zero - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - name: Lock the password of the user accounts other than root with uid 0 ansible.builtin.command: passwd -l {{ item.key }} loop: '{{ getent_passwd | dict2items | rejectattr(''key'', ''search'', ''root'') | list }}' when: item.value.1 == '0' tags: - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-6(5) - NIST-800-53-IA-2 - NIST-800-53-IA-4(b) - PCI-DSS-Req-8.5 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.1 - accounts_no_uid_except_zero - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy Verify Root Has A Primary GID 0 The root user should have a primary group of 0. Req-8.1.1 5.4.2.2 8.2.1 8.2 To help ensure that root-owned files are not inadvertently exposed to other users. Ensure the Group Used by pam_wheel.so Module Exists on System and is Empty Ensure that the group referenced by var_pam_wheel_group_for_su variable and used as value for the pam_wheel.so group option exists and has no members. This empty group used by pam_wheel.so in /etc/pam.d/su ensures that no user can run commands with altered privileges through the su command. Note that this rule just ensures the group exists and has no members. This rule does not configure pam_wheel.so module. The pam_wheel.so module configuration is accomplished by use_pam_wheel_group_for_su rule. 5.2.7 2.2.6 2.2 The su program allows to run commands with a substitute user and group ID. It is commonly used to run commands as the root user. Limiting access to such command is considered a good security practice. Ensure Authentication Required for Single User Mode Single user mode is used for recovery when the system detects an issue during boot or by manual selection from the bootloader. 5.4.2.4 2.2.2 2.2 Requiring authentication in single user mode prevents an unauthorized user from rebooting the system into single user to gain root privileges without credentials. Direct root Logins Not Allowed To further limit access to the root account, administrators can disable root logins at the console by editing the /etc/securetty file. This file lists all devices the root user is allowed to login to. If the file does not exist at all, the root user can login through any communication device on the system, whether via the console or via a raw network interface. This is dangerous as user can login to the system as root via Telnet, which sends the password in plain text over the network. By default, Fedora's /etc/securetty file only allows the root user to login at the console physically attached to the system. To prevent root from logging in, remove the contents of this file. To prevent direct root logins, remove the contents of this file by typing the following command: $ sudo echo > /etc/securetty This rule only checks the /etc/securetty file existence and its content. If you need to restrict user access using the /etc/securetty file, make sure the pam_securetty.so PAM module is properly enabled in relevant PAM files. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.1.1 3.1.6 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 IA-2 CM-6(a) PR.AC-1 PR.AC-6 PR.AC-7 R33 8.6.1 8.6 Disabling direct root logins ensures proper accountability and multifactor authentication to privileged accounts. Users will first login, then escalate to privileged (root) access via su / sudo. This is required for FISMA Low and FISMA Moderate systems. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then echo > /etc/securetty else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.1 - NIST-800-171-3.1.6 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - PCI-DSSv4-8.6 - PCI-DSSv4-8.6.1 - low_complexity - low_disruption - medium_severity - no_direct_root_logins - no_reboot_needed - restrict_strategy - name: Direct root Logins Not Allowed ansible.builtin.copy: dest: /etc/securetty content: '' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.1 - NIST-800-171-3.1.6 - NIST-800-53-CM-6(a) - NIST-800-53-IA-2 - PCI-DSSv4-8.6 - PCI-DSSv4-8.6.1 - low_complexity - low_disruption - medium_severity - no_direct_root_logins - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:, mode: 0600 path: /etc/securetty overwrite: true Ensure that System Accounts Are Locked Some accounts are not associated with a human user of the system, and exist to perform some administrative functions. An attacker should not be able to log into these accounts. System accounts are those user accounts with a user ID less than 1000. If any system account other than root, halt, sync, shutdown and nfsnobody has an unlocked password, disable it with the command: $ sudo usermod -L account CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-6 CM-6(a) 5.4.2.7 8.2.2 8.2 Disabling authentication for default system accounts makes it more difficult for attackers to make use of them to compromise a system. readarray -t systemaccounts < <(awk -F: \ '($3 < 1000 && $3 != root && $3 != halt && $3 != sync && $3 != shutdown \ && $3 != nfsnobody) { print $1 }' /etc/passwd) for systemaccount in "${systemaccounts[@]}"; do usermod -L "$systemaccount" done - name: Ensure that System Accounts Are Locked - Get All Local Users From /etc/passwd ansible.builtin.getent: database: passwd split: ':' tags: - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.2 - low_complexity - medium_disruption - medium_severity - no_password_auth_for_systemaccounts - no_reboot_needed - restrict_strategy - name: Ensure that System Accounts Are Locked - Create local_users Variable From getent_passwd Facts ansible.builtin.set_fact: local_users: '{{ ansible_facts.getent_passwd | dict2items }}' tags: - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.2 - low_complexity - medium_disruption - medium_severity - no_password_auth_for_systemaccounts - no_reboot_needed - restrict_strategy - name: Ensure that System Accounts Are Locked - Lock System Accounts ansible.builtin.user: name: '{{ item.key }}' password_lock: true loop: '{{ local_users }}' when: - item.value[1]|int < 1000 - item.key not in ['root', 'halt', 'sync', 'shutdown', 'nfsnobody'] tags: - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.2 - low_complexity - medium_disruption - medium_severity - no_password_auth_for_systemaccounts - no_reboot_needed - restrict_strategy Restrict Web Browser Use for Administrative Accounts Enforce policy requiring administrative accounts use web browsers only for local service administration. If a browser vulnerability is exploited while running with administrative privileges, the entire system could be compromised. Specific exceptions for local service administration should be documented in site-defined policy. Ensure that System Accounts Do Not Run a Shell Upon Login Some accounts are not associated with a human user of the system, and exist to perform some administrative functions. Should an attacker be able to log into these accounts, they should not be granted access to a shell. The login shell for each local account is stored in the last field of each line in /etc/passwd. System accounts are those user accounts with a user ID less than 1000. The user ID is stored in the third field. If any system account other than root has a login shell, disable it with the command: $ sudo usermod -s /sbin/nologin account Do not perform the steps in this section on the root account. Doing so might cause the system to become inaccessible. 1 12 13 14 15 16 18 3 5 7 8 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS06.03 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 1491 A.12.4.1 A.12.4.3 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 AC-6 CM-6(a) CM-6(b) CM-6.1(iv) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 SRG-OS-000480-GPOS-00227 5.4.2.7 8.2.2 8.2 Ensuring shells are not given to system accounts upon login makes it more difficult for attackers to make use of system accounts. readarray -t systemaccounts < <(awk -F: '($3 < 1000 && $3 != root \ && $7 != "\/sbin\/shutdown" && $7 != "\/sbin\/halt" && $7 != "\/bin\/sync") \ { print $1 }' /etc/passwd) for systemaccount in "${systemaccounts[@]}"; do usermod -s /sbin/nologin "$systemaccount" done - name: Ensure that System Accounts Do Not Run a Shell Upon Login - Get All Local Users From /etc/passwd ansible.builtin.getent: database: passwd split: ':' tags: - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.2 - low_complexity - medium_disruption - medium_severity - no_reboot_needed - no_shelllogin_for_systemaccounts - restrict_strategy - name: Ensure that System Accounts Do Not Run a Shell Upon Login - Create local_users Variable From getent_passwd Facts ansible.builtin.set_fact: local_users: '{{ ansible_facts.getent_passwd | dict2items }}' tags: - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.2 - low_complexity - medium_disruption - medium_severity - no_reboot_needed - no_shelllogin_for_systemaccounts - restrict_strategy - name: Ensure that System Accounts Do Not Run a Shell Upon Login - Disable Login Shell for System Accounts ansible.builtin.user: name: '{{ item.key }}' shell: /sbin/nologin loop: '{{ local_users }}' when: - item.key not in ['root'] - item.value[1]|int < 1000 - item.value[5] not in ['/sbin/shutdown', '/sbin/halt', '/bin/sync'] tags: - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.2 - low_complexity - medium_disruption - medium_severity - no_reboot_needed - no_shelllogin_for_systemaccounts - restrict_strategy Restrict Serial Port Root Logins To restrict root logins on serial ports, ensure lines of this form do not appear in /etc/securetty: ttyS0 ttyS1 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.1 3.1.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-6 CM-6(a) PR.AC-4 PR.DS-5 Preventing direct root login to serial port interfaces helps ensure accountability for actions taken on the systems using the root account. sed -i '/ttyS/d' /etc/securetty - name: Restrict Serial Port Root Logins ansible.builtin.lineinfile: dest: /etc/securetty regexp: ttyS[0-9] state: absent tags: - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_serial_port_logins - restrict_strategy Root Path Must Be Vendor Default Assuming root shell is bash, edit the following files: ~/.profile ~/.bashrc Change any PATH variables to the vendor default for root and remove any empty PATH entries or references to relative paths. 18 APO13.01 BAI03.01 BAI03.02 BAI03.03 4.3.4.3.3 A.14.1.1 A.14.2.1 A.14.2.5 A.6.1.5 CM-6(a) PR.IP-2 The root account's executable search path must be the vendor default, and must contain only absolute paths. Restrict Virtual Console Root Logins To restrict root logins through the (deprecated) virtual console devices, ensure lines of this form do not appear in /etc/securetty: vc/1 vc/2 vc/3 vc/4 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.1 3.1.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-6 CM-6(a) PR.AC-4 PR.DS-5 SRG-OS-000324-GPOS-00125 8.6.1 8.6 Preventing direct root login to virtual console devices helps ensure accountability for actions taken on the system using the root account. sed -i '/^vc\/[0-9]/d' /etc/securetty - name: Restrict Virtual Console Root Logins ansible.builtin.lineinfile: dest: /etc/securetty regexp: ^vc/[0-9] state: absent tags: - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - PCI-DSSv4-8.6 - PCI-DSSv4-8.6.1 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - securetty_root_login_console_only Enforce usage of pam_wheel for su authentication To ensure that only users who are members of the wheel group can run commands with altered privileges through the su command, make sure that the following line exists in the file /etc/pam.d/su: auth required pam_wheel.so use_uid Members of "wheel" or GID 0 groups are checked by default if the group option is not set for pam_wheel.so module. Therefore, members of these groups should be manually checked or a different group should be informed according to the site policy. FMT_SMF_EXT.1.1 SRG-OS-000373-GPOS-00156 SRG-OS-000312-GPOS-00123 The su program allows to run commands with a substitute user and group ID. It is commonly used to run commands as the root user. Limiting access to such command is considered a good security practice. # Remediation is applicable only in certain platforms if rpm --quiet -q pam; then # uncomment the option if commented sed '/^[[:space:]]*#[[:space:]]*auth[[:space:]]\+required[[:space:]]\+pam_wheel\.so[[:space:]]\+use_uid$/s/^[[:space:]]*#//' -i /etc/pam.d/su else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - use_pam_wheel_for_su - name: Restrict usage of su command only to members of wheel group ansible.builtin.replace: path: /etc/pam.d/su regexp: ^[\s]*#[\s]*auth[\s]+required[\s]+pam_wheel\.so[\s]+use_uid$ replace: auth required pam_wheel.so use_uid when: '"pam" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - use_pam_wheel_for_su Enforce Usage of pam_wheel with Group Parameter for su Authentication To ensure that only users who are members of the group set in the group option of pam_wheel.so module can run commands with altered privileges through the su command, make sure that the following line exists in the file /etc/pam.d/su: auth required pam_wheel.so use_uid group= Note that ensure_pam_wheel_group_empty rule complements this requirement by ensuring the referenced group exists and has no members. 5.2.7 2.2.6 2.2 The su program allows to run commands with a substitute user and group ID. It is commonly used to run commands as the root user. Limiting access to such command is considered a good security practice. Secure Session Configuration Files for Login Accounts When a user logs into a Unix account, the system configures the user's session by reading a number of files. Many of these files are located in the user's home directory, and may have weak permissions as a result of user error or misconfiguration. If an attacker can modify or even read certain types of account configuration information, they can often gain full access to the affected user's account. Therefore, it is important to test and correct configuration file permissions for interactive accounts, particularly those of privileged users such as root or system administrators. Maximum login attempts delay Maximum time in seconds between fail login attempts before re-prompting. 1 2 3 4 5 4 Maximum concurrent login sessions Maximum number of concurrent sessions by a user 1 10 15 20 3 5 1 Account Inactivity Timeout (seconds) In an interactive shell, the value is interpreted as the number of seconds to wait for input after issuing the primary prompt. Bash terminates after waiting for that number of seconds if input does not arrive. 1800 600 900 300 600 Interactive users initialization files 'A regular expression describing a list of file names for files that are sourced at login time for interactive users' ^(\.bashrc|\.zshrc|\.cshrc|\.profile|\.bash_login|\.bash_profile)$ ^\.[\w\- ]+$ Ensure Home Directories are Created for New Users All local interactive user accounts, upon creation, should be assigned a home directory. Configure the operating system to assign home directories to all new local interactive users by setting the CREATE_HOME parameter in /etc/login.defs to yes as follows: CREATE_HOME yes SRG-OS-000480-GPOS-00227 If local interactive users are not assigned a valid home directory, there is no place for the storage and control of files they should own. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then if [ -e "/etc/login.defs" ] ; then LC_ALL=C sed -i "/^\s*CREATE_HOME\s\+/Id" "/etc/login.defs" else touch "/etc/login.defs" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/login.defs" cp "/etc/login.defs" "/etc/login.defs.bak" # Insert before the line matching the regex '^\s*CREATE_HOME'. line_number="$(LC_ALL=C grep -n "^\s*CREATE_HOME" "/etc/login.defs.bak" | LC_ALL=C sed 's/:.*//g')" if [ -z "$line_number" ]; then # There was no match of '^\s*CREATE_HOME', insert at # the end of the file. printf '%s\n' "CREATE_HOME yes" >> "/etc/login.defs" else head -n "$(( line_number - 1 ))" "/etc/login.defs.bak" > "/etc/login.defs" printf '%s\n' "CREATE_HOME yes" >> "/etc/login.defs" tail -n "+$(( line_number ))" "/etc/login.defs.bak" >> "/etc/login.defs" fi # Clean up after ourselves. rm "/etc/login.defs.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - accounts_have_homedir_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure new users receive home directories block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/login.defs create: true regexp: (?i)^\s*CREATE_HOME\s+ state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/login.defs ansible.builtin.lineinfile: path: /etc/login.defs create: true regexp: (?i)^\s*CREATE_HOME\s+ state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/login.defs ansible.builtin.lineinfile: path: /etc/login.defs create: true regexp: (?i)^\s*CREATE_HOME\s+ line: CREATE_HOME yes state: present when: '"shadow-utils" in ansible_facts.packages' tags: - accounts_have_homedir_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure the Logon Failure Delay is Set Correctly in login.defs To ensure the logon failure delay controlled by /etc/login.defs is set properly, add or correct the FAIL_DELAY setting in /etc/login.defs to read as follows: FAIL_DELAY 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 AC-7(b) CM-6(a) PR.IP-1 SRG-OS-000480-GPOS-00226 Increasing the time between a failed authentication attempt and re-prompting to enter credentials helps to slow a single-threaded brute force attack. Limit the Number of Concurrent Login Sessions Allowed Per User Limiting the number of allowed users and sessions per user can limit risks related to Denial of Service attacks. This addresses concurrent sessions for a single account and does not address concurrent sessions by a single user via multiple accounts. To set the number of concurrent sessions per user add the following line in /etc/security/limits.conf or a file under /etc/security/limits.d/: * hard maxlogins 14 15 18 9 5.5.2.2 DSS01.05 DSS05.02 4.3.3.4 SR 3.1 SR 3.8 A.13.1.1 A.13.1.3 A.13.2.1 A.14.1.2 A.14.1.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 AC-10 CM-6(a) PR.AC-5 SRG-OS-000027-GPOS-00008 Limiting simultaneous user logins can insulate the system from denial of service problems caused by excessive logins. Automated login processes operating improperly or maliciously may result in an exceptional number of simultaneous login sessions. - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-53-AC-10 - NIST-800-53-CM-6(a) - accounts_max_concurrent_login_sessions - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_max_concurrent_login_sessions # promote to variable set_fact: var_accounts_max_concurrent_login_sessions: !!str tags: - always - name: Find /etc/security/limits.d files containing maxlogins configuration ansible.builtin.find: paths: /etc/security/limits.d contains: ^[\s]*\*[\s]+(?:(?:hard)|(?:-))[\s]+maxlogins patterns: '*.conf' register: maxlogins when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-10 - NIST-800-53-CM-6(a) - accounts_max_concurrent_login_sessions - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Limit the Number of Concurrent Login Sessions Allowed Per User in files from limits.d ansible.builtin.replace: dest: '{{ item.path }}' regexp: ^#?\*.*maxlogins.* replace: '* hard maxlogins {{ var_accounts_max_concurrent_login_sessions }}' with_items: - '{{ maxlogins.files }}' when: '"pam" in ansible_facts.packages' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-10 - NIST-800-53-CM-6(a) - accounts_max_concurrent_login_sessions - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Limit the Number of Concurrent Login Sessions Allowed Per User ansible.builtin.lineinfile: state: present dest: /etc/security/limits.conf insertbefore: ^# End of file regexp: ^#?\*.*maxlogins line: '* hard maxlogins {{ var_accounts_max_concurrent_login_sessions }}' create: true when: - '"pam" in ansible_facts.packages' - maxlogins.matched == 0 tags: - CJIS-5.5.2.2 - NIST-800-53-AC-10 - NIST-800-53-CM-6(a) - accounts_max_concurrent_login_sessions - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy Configure Polyinstantiation of /tmp Directories To configure polyinstantiated /tmp directories, first create the parent directories which will hold the polyinstantiation child directories. Use the following command: $ sudo mkdir --mode 000 /tmp/tmp-inst Then, add the following entry to /etc/security/namespace.conf: /tmp /tmp/tmp-inst/ level root,adm R55 Polyinstantiation of temporary directories is a proactive security measure which reduces chances of attacks that are made possible by /tmp directories being world-writable. # shellcheck disable=SC2174 mkdir -p --mode 000 /tmp/tmp-inst chmod 000 /tmp/tmp-inst chcon --reference=/tmp /tmp/tmp-inst if ! grep -Eq '^\s*/tmp\s+/tmp/tmp-inst/\s+level\s+root,adm$' /etc/security/namespace.conf ; then if grep -Eq '^\s*/tmp\s+' /etc/security/namespace.conf ; then sed -i '/^\s*\/tmp/d' /etc/security/namespace.conf fi echo "/tmp /tmp/tmp-inst/ level root,adm" >> /etc/security/namespace.conf fi - name: Create /tmp/tmp-inst directory ansible.builtin.file: path: /tmp/tmp-inst state: directory mode: '000' seuser: system_u serole: object_r setype: tmp_t tags: - accounts_polyinstantiated_tmp - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Make changes to /etc/security/namespace.conf ansible.builtin.lineinfile: path: /etc/security/namespace.conf create: false regexp: ^\s*/tmp\s+/tmp/tmp-inst/\s+level\s+root,adm$ line: /tmp /tmp/tmp-inst/ level root,adm state: present tags: - accounts_polyinstantiated_tmp - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy Configure Polyinstantiation of /var/tmp Directories To configure polyinstantiated /tmp directories, first create the parent directories which will hold the polyinstantiation child directories. Use the following command: $ sudo mkdir --mode 000 /var/tmp/tmp-inst Then, add the following entry to /etc/security/namespace.conf: /var/tmp /var/tmp/tmp-inst/ level root,adm R55 Polyinstantiation of temporary directories is a proactive security measure which reduces chances of attacks that are made possible by /var/tmp directories being world-writable. # shellcheck disable=SC2174 mkdir -p --mode 000 /var/tmp/tmp-inst chmod 000 /var/tmp/tmp-inst chcon --reference=/var/tmp /var/tmp/tmp-inst if ! grep -Eq '^\s*/var/tmp\s+/var/tmp/tmp-inst/\s+level\s+root,adm$' /etc/security/namespace.conf ; then if grep -Eq '^\s*/var/tmp\s+' /etc/security/namespace.conf ; then sed -i '/^\s*\/var\/tmp/d' /etc/security/namespace.conf fi echo "/var/tmp /var/tmp/tmp-inst/ level root,adm" >> /etc/security/namespace.conf fi - name: Create /var/tmp/tmp-inst directory ansible.builtin.file: path: /var/tmp/tmp-inst state: directory mode: '000' seuser: system_u serole: object_r setype: tmp_t tags: - accounts_polyinstantiated_var_tmp - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Make changes to /etc/security/namespace.conf ansible.builtin.lineinfile: path: /etc/security/namespace.conf create: false regexp: ^\s*/var/tmp\s+/var/tmp/tmp-inst/\s+level\s+root,adm$ line: /var/tmp /var/tmp/tmp-inst/ level root,adm state: present tags: - accounts_polyinstantiated_var_tmp - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy Set Interactive Session Timeout Setting the TMOUT option in /etc/profile ensures that all user sessions will terminate based on inactivity. A value of 0 (zero) disables the automatic logout feature and is therefore not a compliant setting. The value of TMOUT should be a positive integer, exported, and read only. The TMOUT setting in a file loaded by /etc/profile, e.g. /etc/profile.d/tmout.sh should read as follows: typeset -xr TMOUT= or declare -xr TMOUT= Using the typeset keyword is preferred for wider compatibility with ksh and other shells. 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 3.1.11 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 CIP-004-6 R2.2.3 CIP-007-3 R5.1 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-12 SC-10 AC-2(5) CM-6(a) PR.AC-7 SRG-OS-000163-GPOS-00072 SRG-OS-000029-GPOS-00010 R32 5.4.3.2 8.6.1 8.6 Terminating an idle session within a short time period reduces the window of opportunity for unauthorized personnel to take control of a management session enabled on the console or console port that has been left unattended. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_accounts_tmout='' # if 0, no occurence of tmout found, if 1, occurence found tmout_found=0 for f in /etc/profile /etc/profile.d/*.sh; do if grep --silent '^[^#].*TMOUT' $f; then sed -i -E "s/^(.*)TMOUT\s*=\s*(\w|\$)*(.*)$/typeset -xr TMOUT=$var_accounts_tmout\3/g" $f tmout_found=1 fi done if [ $tmout_found -eq 0 ]; then echo -e "\n# Set TMOUT to $var_accounts_tmout per security requirements" >> /etc/profile.d/tmout.sh echo "typeset -xr TMOUT=$var_accounts_tmout" >> /etc/profile.d/tmout.sh fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSSv4-8.6 - PCI-DSSv4-8.6.1 - accounts_tmout - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_tmout # promote to variable set_fact: var_accounts_tmout: !!str tags: - always - name: Correct any occurrence of TMOUT in /etc/profile ansible.builtin.replace: path: /etc/profile regexp: ^[^#].*TMOUT=.* replace: typeset -xr TMOUT={{ var_accounts_tmout }} register: profile_replaced when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSSv4-8.6 - PCI-DSSv4-8.6.1 - accounts_tmout - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set Interactive Session Timeout ansible.builtin.lineinfile: path: /etc/profile.d/tmout.sh create: true regexp: TMOUT= line: typeset -xr TMOUT={{ var_accounts_tmout }} state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSSv4-8.6 - PCI-DSSv4-8.6.1 - accounts_tmout - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy User Initialization Files Must Be Group-Owned By The Primary Group Change the group owner of interactive users files to the group found in /etc/passwd for the user. To change the group owner of a local interactive user home directory, use the following command: $ sudo chgrp USER_GROUP /home/USER/.INIT_FILE This rule ensures every initialization file related to an interactive user is group-owned by an interactive user. Due to OVAL limitation, this rule can report a false negative in a specific situation where two interactive users swap the group-ownership of their respective initialization files. SRG-OS-000480-GPOS-00227 R50 7.2.9 Local initialization files for interactive users are used to configure the user's shell environment upon logon. Malicious modification of these files could compromise accounts upon logon. awk -F: '{if ($4 >= 1000 && $4 != 65534) print $4":"$6}' /etc/passwd | while IFS=: read -r gid home; do find -P "$home" -maxdepth 1 -type f -name "\.[^.]*" -exec chgrp -f --no-dereference -- $gid "{}" \;; done - name: Ensure interactive local users are the group-owners of their respective initialization files ansible.builtin.shell: cmd: |2- awk -F: '{if ($4 >= 1000 && $4 != 65534) print $4":"$6}' /etc/passwd | while IFS=: read -r gid home; do find -P "$home" -maxdepth 1 -type f -name "\.[^.]*" -exec chgrp -f --no-dereference -- $gid "{}" \;; done tags: - accounts_user_dot_group_ownership - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy User Initialization Files Must Not Run World-Writable Programs Set the mode on files being executed by the user initialization files with the following command: $ sudo chmod o-w FILE SRG-OS-000480-GPOS-00227 7.2.9 If user start-up files execute world-writable programs, especially in unprotected directories, they could be maliciously modified to destroy user files or otherwise compromise the system at the user level. If the system is compromised at the user level, it is easier to elevate privileges to eventually compromise the system at the root and network level. readarray -t world_writable_files < <(find / -xdev -type f -perm -0002 2> /dev/null) readarray -t interactive_home_dirs < <(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $6 }' /etc/passwd) for world_writable in "${world_writable_files[@]}"; do for homedir in "${interactive_home_dirs[@]}"; do if grep -q -d skip "$world_writable" "$homedir"/.*; then chmod o-w $world_writable break fi done done - name: User Initialization Files Must Not Run World-Writable Programs - Initialize variables ansible.builtin.set_fact: home_user_dirs: [] world_writable_files: [] tags: - accounts_user_dot_no_world_writable_programs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: User Initialization Files Must Not Run World-Writable Programs - Get user's home dir list ansible.builtin.getent: database: passwd register: passwd_database tags: - accounts_user_dot_no_world_writable_programs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: User Initialization Files Must Not Run World-Writable Programs - Fill home_user_dirs ansible.builtin.set_fact: home_user_dirs: '{{ home_user_dirs + [item.data[4]] }}' when: item.data[4] is defined and item.data[2]|int >= 1000 and item.data[2]|int != 65534 with_items: '{{ passwd_database.ansible_facts.getent_passwd | dict2items(key_name=''user'', value_name=''data'')}}' tags: - accounts_user_dot_no_world_writable_programs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: User Initialization Files Must Not Run World-Writable Programs - Get world writable files ansible.builtin.shell: | find / -xdev -type f -perm -0002 2> /dev/null register: world_writable_files tags: - accounts_user_dot_no_world_writable_programs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: User Initialization Files Must Not Run World-Writable Programs - Find referenced_files in init files ansible.builtin.find: paths: '{{ home_user_dirs }}' contains: '{{ item }}' hidden: true read_whole_file: true recurse: true with_items: '{{ world_writable_files.stdout_lines }}' register: referenced_files tags: - accounts_user_dot_no_world_writable_programs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: User Initialization Files Must Not Run World-Writable Programs - Remove world writable permissions ansible.builtin.file: path: '{{ item.item }}' mode: o-w when: item.matched > 0 with_items: '{{ referenced_files.results }}' tags: - accounts_user_dot_no_world_writable_programs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy User Initialization Files Must Be Owned By the Primary User Set the owner of the user initialization files for interactive users to the primary owner with the following command: $ sudo chown USER /home/USER/.* This rule ensures every initialization file related to an interactive user is owned by an interactive user. Due to OVAL limitation, this rule can report a false negative in a specific situation where two interactive users swap the ownership of their respective initialization files. SRG-OS-000480-GPOS-00227 R50 7.2.9 Local initialization files are used to configure the user's shell environment upon logon. Malicious modification of these files could compromise accounts upon logon. awk -F: '{if ($3 >= 1000 && $3 != 65534) print $3":"$6}' /etc/passwd | while IFS=: read -r uid home; do find -P "$home" -maxdepth 1 -type f -name "\.[^.]*" -exec chown -f --no-dereference -- $uid "{}" \;; done - name: Ensure interactive local users are the owners of their respective initialization files ansible.builtin.shell: cmd: 'awk -F: ''{if ($3 >= 1000 && $3 != 65534) print $3":"$6}'' /etc/passwd | while IFS=: read -r uid home; do find -P "$home" -maxdepth 1 -type f -name "\.[^.]*" -exec chown -f --no-dereference -- $uid "{}" \;; done' tags: - accounts_user_dot_user_ownership - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy All Interactive Users Home Directories Must Exist Create home directories to all local interactive users that currently do not have a home directory assigned. Use the following commands to create the user home directory assigned in /etc/passwd: $ sudo mkdir /home/USER SRG-OS-000480-GPOS-00227 7.2.8 If a local interactive user has a home directory defined that does not exist, the user may be given access to the / directory as the current working directory upon logon. This could create a Denial of Service because the user would not be able to access their logon configuration files, and it may give them visibility to system files they normally would not be able to access. for user in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $1}' /etc/passwd); do mkhomedir_helper $user 0077; done - name: Get all local users from /etc/passwd ansible.builtin.getent: database: passwd split: ':' tags: - accounts_user_interactive_home_directory_exists - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Create local_users variable from the getent output ansible.builtin.set_fact: local_users: '{{ ansible_facts.getent_passwd|dict2items }}' tags: - accounts_user_interactive_home_directory_exists - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure interactive users have a home directory exists ansible.builtin.user: name: '{{ item.key }}' create_home: true loop: '{{ local_users }}' when: - item.value[1]|int >= 1000 - item.value[1]|int != 65534 tags: - accounts_user_interactive_home_directory_exists - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy All Interactive User Home Directories Must Be Group-Owned By The Primary Group Change the group owner of interactive users home directory to the group found in /etc/passwd. To change the group owner of interactive users home directory, use the following command: $ sudo chgrp USER_GROUP /home/USER This rule ensures every home directory related to an interactive user is group-owned by an interactive user. It also ensures that interactive users are group-owners of one and only one home directory. Due to OVAL limitation, this rule can report a false negative in a specific situation where two interactive users swap the group-ownership of their respective home directories. SRG-OS-000480-GPOS-00227 If the Group Identifier (GID) of a local interactive users home directory is not the same as the primary GID of the user, this would allow unauthorized access to the users files, and users that share the same group may not be able to access files that they legitimately should. awk -F':' '{ if ($3 >= 1000 && $3 != 65534) system("chgrp -f " $4" "$6) }' /etc/passwd - name: Get all local users from /etc/passwd ansible.builtin.getent: database: passwd split: ':' tags: - file_groupownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Create local_users variable from the getent output ansible.builtin.set_fact: local_users: '{{ ansible_facts.getent_passwd|dict2items }}' tags: - file_groupownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Test for existence of home directories to avoid creating them, but only fixing group ownership ansible.builtin.stat: path: '{{ item.value[4] }}' register: path_exists loop: '{{ local_users }}' when: - item.value[1]|int >= 1000 - item.value[1]|int != 65534 tags: - file_groupownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure interactive local users are the group-owners of their respective home directories ansible.builtin.file: path: '{{ item.0.value[4] }}' group: '{{ item.0.value[2] }}' loop: '{{ local_users|zip(path_exists.results)|list }}' when: item.1.stat is defined and item.1.stat.exists tags: - file_groupownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy All Interactive User Home Directories Must Be Owned By The Primary User Change the owner of interactive users home directories to that correct owner. To change the owner of a interactive users home directory, use the following command: $ sudo chown USER /home/USER This rule ensures every home directory related to an interactive user is owned by an interactive user. It also ensures that interactive users are owners of one and only one home directory. Due to OVAL limitation, this rule can report a false negative in a specific situation where two interactive users swap the ownership of their respective home directories. SRG-OS-000480-GPOS-00227 7.2.8 If a local interactive user does not own their home directory, unauthorized users could access or modify the user's files, and the users may not be able to access their own files. awk -F':' '{ if ($3 >= 1000 && $3 != 65534) system("chown -f " $3" "$6) }' /etc/passwd - name: Get all local users from /etc/passwd ansible.builtin.getent: database: passwd split: ':' tags: - file_ownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Create local_users variable from the getent output ansible.builtin.set_fact: local_users: '{{ ansible_facts.getent_passwd|dict2items }}' tags: - file_ownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Test for existence of home directories to avoid creating them, but only fixing ownership ansible.builtin.stat: path: '{{ item.value[4] }}' register: path_exists loop: '{{ local_users }}' when: - item.value[1]|int >= 1000 - item.value[1]|int != 65534 tags: - file_ownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure interactive local users are the owners of their respective home directories ansible.builtin.file: path: '{{ item.0.value[4] }}' owner: '{{ item.0.value[1] }}' loop: '{{ local_users|zip(path_exists.results)|list }}' when: item.1.stat is defined and item.1.stat.exists tags: - file_ownership_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure All User Initialization Files Have Mode 0740 Or Less Permissive Set the mode of the user initialization files to 0740 with the following command: $ sudo chmod 0740 /home/USER/.INIT_FILE SRG-OS-000480-GPOS-00227 R50 7.2.9 Local initialization files are used to configure the user's shell environment upon logon. Malicious modification of these files could compromise accounts upon logon. var_user_initialization_files_regex='' readarray -t interactive_users < <(awk -F: '$3>=1000 {print $1}' /etc/passwd) readarray -t interactive_users_home < <(awk -F: '$3>=1000 {print $6}' /etc/passwd) readarray -t interactive_users_shell < <(awk -F: '$3>=1000 {print $7}' /etc/passwd) USERS_IGNORED_REGEX='nobody|nfsnobody' for (( i=0; i<"${#interactive_users[@]}"; i++ )); do if ! grep -qP "$USERS_IGNORED_REGEX" <<< "${interactive_users[$i]}" && \ [ "${interactive_users_shell[$i]}" != "/sbin/nologin" ]; then readarray -t init_files < <(find "${interactive_users_home[$i]}" -maxdepth 1 \ -exec basename {} \; | grep -P "$var_user_initialization_files_regex") for file in "${init_files[@]}"; do chmod u-s,g-wxs,o= "${interactive_users_home[$i]}/$file" done fi done - name: XCCDF Value var_user_initialization_files_regex # promote to variable set_fact: var_user_initialization_files_regex: !!str tags: - always - name: Ensure All User Initialization Files Have Mode 0740 Or Less Permissive - Gather User Info ansible.builtin.getent: database: passwd tags: - file_permission_user_init_files - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All User Initialization Files Have Mode 0740 Or Less Permissive - Find Init Files ansible.builtin.find: paths: '{{ item.value[4] }}' pattern: '{{ var_user_initialization_files_regex }}' hidden: true use_regex: true with_dict: '{{ ansible_facts.getent_passwd }}' when: - item.value[4] != "/sbin/nologin" - item.key not in ["nobody", "nfsnobody"] - item.value[1] | int >= 1000 register: found_init_files tags: - file_permission_user_init_files - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All User Initialization Files Have Mode 0740 Or Less Permissive - Fix Init Files Permissions ansible.builtin.file: path: '{{ item.1.path }}' mode: u-s,g-wxs,o= loop: '{{ q(''ansible.builtin.subelements'', found_init_files.results, ''files'', {''skip_missing'': True}) }}' tags: - file_permission_user_init_files - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy All Interactive User Home Directories Must Have mode 0750 Or Less Permissive Change the mode of interactive users home directories to 0750. To change the mode of interactive users home directory, use the following command: $ sudo chmod 0750 /home/USER SRG-OS-000480-GPOS-00227 7.2.8 Excessive permissions on local interactive user home directories may allow unauthorized access to user files by other users. for home_dir in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534 && $6 != "/") print $6 }' /etc/passwd); do # Only update the permissions when necessary. This will avoid changing the inode timestamp when # the permission is already defined as expected, therefore not impacting in possible integrity # check systems that also check inodes timestamps. find "$home_dir" -maxdepth 0 -perm /7027 \! -type l -exec chmod u-s,g-w-s,o=- {} \; done - name: Get all local users from /etc/passwd ansible.builtin.getent: database: passwd split: ':' tags: - file_permissions_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Create local_users variable from the getent output ansible.builtin.set_fact: local_users: '{{ ansible_facts.getent_passwd|dict2items }}' tags: - file_permissions_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Test for existence home directories to avoid creating them. ansible.builtin.stat: path: '{{ item.value[4] }}' register: path_exists loop: '{{ local_users }}' when: - item.value[1]|int >= 1000 - item.value[1]|int != 65534 - item.value[4] != "/" tags: - file_permissions_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure interactive local users have proper permissions on their respective home directories ansible.builtin.file: path: '{{ item.0.value[4] }}' mode: u-s,g-w-s,o=- follow: false recurse: false loop: '{{ local_users|zip(path_exists.results)|list }}' when: item.1.stat is defined and item.1.stat.exists tags: - file_permissions_home_directories - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure that User Home Directories are not Group-Writable or World-Readable For each human user of the system, view the permissions of the user's home directory: # ls -ld /home/USER Ensure that the directory is not group-writable and that it is not world-readable. If necessary, repair the permissions: # chmod g-w /home/USER # chmod o-rwx /home/USER This action may involve modifying user home directories. Notify your user community, and solicit input if appropriate, before making this type of change. This rule is deprecated in favor of the file_permissions_home_directories rule.Please consider replacing this rule in your files as it is not expected to receive updates as of version 0.1.62. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) CM-6(a) PR.AC-4 PR.DS-5 User home directories contain many configuration files which affect the behavior of a user's account. No user should ever have write permission to another user's home directory. Group shared directories can be configured in sub-directories or elsewhere in the filesystem if they are needed. Typically, user home directories should not be world-readable, as it would disclose file names to other users. If a subset of users need read access to one another's home directories, this can be provided using groups or ACLs. for home_dir in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534 && $6 != "/") print $6 }' /etc/passwd); do # Only update the permissions when necessary. This will avoid changing the inode timestamp when # the permission is already defined as expected, therefore not impacting in possible integrity # check systems that also check inodes timestamps. find "$home_dir" -maxdepth 0 -perm /7027 \! -type l -exec chmod u-s,g-w-s,o=- {} \; done - name: Get all local users from /etc/passwd ansible.builtin.getent: database: passwd split: ':' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - file_permissions_home_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Create local_users variable from the getent output ansible.builtin.set_fact: local_users: '{{ ansible_facts.getent_passwd|dict2items }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - file_permissions_home_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Test for existence home directories to avoid creating them. ansible.builtin.stat: path: '{{ item.value[4] }}' register: path_exists loop: '{{ local_users }}' when: - item.value[1]|int >= 1000 - item.value[1]|int != 65534 - item.value[4] != "/" tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - file_permissions_home_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure interactive local users have proper permissions on their respective home directories ansible.builtin.file: path: '{{ item.0.value[4] }}' mode: u-s,g-w-s,o=- follow: false recurse: false loop: '{{ local_users|zip(path_exists.results)|list }}' when: item.1.stat is defined and item.1.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - file_permissions_home_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure that No Dangerous Directories Exist in Root's Path The active path of the root account can be obtained by starting a new root shell and running: # echo $PATH This will produce a colon-separated list of directories in the path. Certain path elements could be considered dangerous, as they could lead to root executing unknown or untrusted programs, which could contain malicious code. Since root may sometimes work inside untrusted directories, the . character, which represents the current directory, should never be in the root path, nor should any directory which can be written to by an unprivileged or semi-privileged (system) user. It is a good practice for administrators to always execute privileged commands by typing the full path to the command. Ensure that Root's Path Does Not Include World or Group-Writable Directories For each element in root's path, run: # ls -ld DIR and ensure that write permissions are disabled for group and other. 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-6(a) CM-6(a) PR.IP-1 5.4.2.5 Such entries increase the risk that root could execute code provided by unprivileged users, and potentially malicious code. - name: Get root paths which are not symbolic links ansible.builtin.stat: path: '{{ item }}' changed_when: false failed_when: false register: root_paths with_items: '{{ ansible_env.PATH.split('':'') }}' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - accounts_root_path_dirs_no_write - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable writability to root directories ansible.builtin.file: path: '{{ item.item }}' mode: g-w,o-w with_items: '{{ root_paths.results }}' when: - root_paths.results is defined - item.stat.exists - not item.stat.islnk tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - accounts_root_path_dirs_no_write - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure that Root's Path Does Not Include Relative Paths or Null Directories Ensure that none of the directories in root's path is equal to a single . character, or that it contains any instances that lead to relative path traversal, such as .. or beginning a path without the slash (/) character. Also ensure that there are no "empty" elements in the path, such as in these examples: PATH=:/bin PATH=/bin: PATH=/bin::/sbin These empty elements have the same effect as a single . character. 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-6(a) CM-6(a) PR.IP-1 5.4.2.5 Including these entries increases the risk that root could execute code from an untrusted location. Ensure that Users Have Sensible Umask Values The umask setting controls the default permissions for the creation of new files. With a default umask setting of 077, files and directories created by users will not be readable by any other user on the system. Users who wish to make specific files group- or world-readable can accomplish this by using the chmod command. Additionally, users can make all their files readable to their group by default by setting a umask of 027 in their shell configuration files. If default per-user groups exist (that is, if every user has a default group whose name is the same as that user's username and whose only member is the user), then it may even be safe for users to select a umask of 007, making it very easy to intentionally share files with groups of which the user is a member. Sensible umask Enter default user umask 007 022 027 077 027 Ensure the Default Bash Umask is Set Correctly To ensure the default umask for users of the Bash shell is set properly, add or correct the umask setting in /etc/bashrc to read as follows: umask 18 APO13.01 BAI03.01 BAI03.02 BAI03.03 4.3.4.3.3 A.14.1.1 A.14.2.1 A.14.2.5 A.6.1.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-6(1) CM-6(a) PR.IP-2 SRG-OS-000480-GPOS-00228 SRG-OS-000480-GPOS-00227 R36 5.4.3.3 The umask value influences the permissions assigned to files when they are created. A misconfigured umask value could result in files with excessive permissions that can be read or written to by unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q bash; then var_accounts_user_umask='' grep -q "^[^#]*\bumask" /etc/bashrc && \ sed -i -E -e "s/^([^#]*\bumask)[[:space:]]+[[:digit:]]+/\1 $var_accounts_user_umask/g" /etc/bashrc if ! [ $? -eq 0 ]; then echo "umask $var_accounts_user_umask" >> /etc/bashrc fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_bashrc - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_user_umask # promote to variable set_fact: var_accounts_user_umask: !!str tags: - always - name: Check if umask in /etc/bashrc is already set ansible.builtin.lineinfile: path: /etc/bashrc regexp: ^[^#]*\bumask\s+\d+$ state: absent check_mode: true changed_when: false register: umask_replace when: '"bash" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_bashrc - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Replace user umask in /etc/bashrc ansible.builtin.replace: path: /etc/bashrc regexp: ^([^#]*\b)umask\s+\d+$ replace: \g<1>umask {{ var_accounts_user_umask }} when: - '"bash" in ansible_facts.packages' - umask_replace.found > 0 tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_bashrc - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure the Default umask is Appended Correctly ansible.builtin.lineinfile: create: true path: /etc/bashrc line: umask {{ var_accounts_user_umask }} when: - '"bash" in ansible_facts.packages' - umask_replace.found == 0 tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_bashrc - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure the Default Umask is Set Correctly in login.defs To ensure the default umask controlled by /etc/login.defs is set properly, add or correct the UMASK setting in /etc/login.defs to read as follows: UMASK 11 18 3 9 APO13.01 BAI03.01 BAI03.02 BAI03.03 BAI10.01 BAI10.02 BAI10.03 BAI10.05 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.1.1 A.14.2.1 A.14.2.2 A.14.2.3 A.14.2.4 A.14.2.5 A.6.1.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-6(1) CM-6(a) PR.IP-1 PR.IP-2 SRG-OS-000480-GPOS-00228 R36 5.4.3.3 The umask value influences the permissions assigned to files when they are created. A misconfigured umask value could result in files with excessive permissions that can be read and written to by unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q shadow-utils; then var_accounts_user_umask='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^UMASK") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_user_umask" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^UMASK\\>" "/etc/login.defs"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^UMASK\\>.*/$escaped_formatted_output/gi" "/etc/login.defs" else if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs" fi printf '%s\n' "$formatted_output" >> "/etc/login.defs" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_user_umask # promote to variable set_fact: var_accounts_user_umask: !!str tags: - always - name: Check if UMASK is already set ansible.builtin.lineinfile: path: /etc/login.defs regexp: ^(\s*)UMASK\s+.* state: absent check_mode: true changed_when: false register: result_umask_is_set when: '"shadow-utils" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Replace user UMASK in /etc/login.defs ansible.builtin.replace: path: /etc/login.defs regexp: ^(\s*)UMASK(\s+).* replace: \g<1>UMASK\g<2>{{ var_accounts_user_umask }} when: - '"shadow-utils" in ansible_facts.packages' - result_umask_is_set.found > 0 tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure the Default UMASK is Appended Correctly ansible.builtin.lineinfile: create: true path: /etc/login.defs line: UMASK {{ var_accounts_user_umask }} when: - '"shadow-utils" in ansible_facts.packages' - result_umask_is_set.found == 0 tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_login_defs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure the Default Umask is Set Correctly in /etc/profile To ensure the default umask controlled by /etc/profile is set properly, add or correct the umask setting in /etc/profile to read as follows: umask Note that /etc/profile also reads scrips within /etc/profile.d directory. These scripts are also valid files to set umask value. Therefore, they should also be considered during the check and properly remediated, if necessary. 18 APO13.01 BAI03.01 BAI03.02 BAI03.03 4.3.4.3.3 A.14.1.1 A.14.2.1 A.14.2.5 A.6.1.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-6(1) CM-6(a) PR.IP-2 SRG-OS-000480-GPOS-00228 SRG-OS-000480-GPOS-00227 R36 5.4.3.3 The umask value influences the permissions assigned to files when they are created. A misconfigured umask value could result in files with excessive permissions that can be read or written to by unauthorized users. var_accounts_user_umask='' readarray -t profile_files < <(find /etc/profile.d/ -type f -name '*.sh' -or -name 'sh.local') for file in "${profile_files[@]}" /etc/profile; do grep -qE '^[^#]*umask' "$file" && sed -i -E "s/^(\s*umask\s*)[0-7]+/\1$var_accounts_user_umask/g" "$file" done if ! grep -qrE '^[^#]*umask' /etc/profile*; then echo "umask $var_accounts_user_umask" >> /etc/profile fi - name: XCCDF Value var_accounts_user_umask # promote to variable set_fact: var_accounts_user_umask: !!str tags: - always - name: Ensure the Default Umask is Set Correctly in /etc/profile - Locate Profile Configuration Files Where umask Is Defined ansible.builtin.find: paths: - /etc/profile.d patterns: - sh.local - '*.sh' contains: ^[\s]*umask\s+\d+ register: result_profile_d_files tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_profile - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure the Default Umask is Set Correctly in /etc/profile - Replace Existing umask Value in Files From /etc/profile.d ansible.builtin.replace: path: '{{ item.path }}' regexp: ^(\s*)umask\s+\d+ replace: \1umask {{ var_accounts_user_umask }} loop: '{{ result_profile_d_files.files }}' register: result_umask_replaced_profile_d when: result_profile_d_files.matched tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_profile - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure the Default Umask is Set Correctly in /etc/profile - Ensure umask Is Set in /etc/profile if Not Already Set Elsewhere ansible.builtin.lineinfile: create: true mode: 420 path: /etc/profile line: umask {{ var_accounts_user_umask }} when: not result_profile_d_files.matched tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_profile - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure the Default Umask is Set Correctly in /etc/profile - Ensure umask Value For All Existing umask Definition in /etc/profile ansible.builtin.replace: path: /etc/profile regexp: ^(\s*)umask\s+\d+ replace: \1umask {{ var_accounts_user_umask }} register: result_umask_replaced_profile tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - accounts_umask_etc_profile - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy GRUB2 bootloader configuration During the boot process, the boot loader is responsible for starting the execution of the kernel and passing options to it. The boot loader allows for the selection of different kernels - possibly on different partitions or media. The default Fedora boot loader for x86 systems is called GRUB2. Options it can pass to the kernel include single-user mode, which provides root access without any authentication, and the ability to disable SELinux. To prevent local users from modifying the boot parameters and endangering security, protect the boot loader configuration with a password and ensure its configuration file's permissions are set properly. L1TF vulnerability mitigation Defines the L1TF vulneratility mitigations to employ. flush full full,force flush flush,nosmt flush,nowarn MDS vulnerability mitigation Defines the MDS vulneratility mitigation to employ. full full full,nosmt Confidence level on Hardware Random Number Generator Defines the level of trust on the hardware random number generators available in the system and the percentage of entropy to credit. 500 500 512 1000 Spec Store Bypass Mitigation This controls how the Speculative Store Bypass (SSB) vulnerability is mitigated. prctl on auto prctl seccomp Disable Recovery Booting Fedora systems support an "recovery boot" option that can be used to prevent services from being started. The GRUB_DISABLE_RECOVERY configuration option in /etc/default/grub should be set to true to disable the generation of recovery mode menu entries. It is also required to change the runtime configuration, run: $ sudo grubby --update-kernel=ALL FIA_UAU.1 Using recovery boot, the console user could disable auditing, firewalls, or other services, weakening system security. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if grep -q '^GRUB_DISABLE_RECOVERY=.*' '/etc/default/grub' ; then sed -i 's/GRUB_DISABLE_RECOVERY=.*/GRUB_DISABLE_RECOVERY=true/' "/etc/default/grub" else echo "GRUB_DISABLE_RECOVERY=true" >> '/etc/default/grub' fi grubby --update-kernel=ALL else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_disable_recovery - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Verify GRUB_DISABLE_RECOVERY=true ansible.builtin.lineinfile: path: /etc/default/grub regexp: ^GRUB_DISABLE_RECOVERY=.* line: GRUB_DISABLE_RECOVERY=true state: present when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_disable_recovery - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_disable_recovery - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy IOMMU configuration directive On x86 architecture supporting VT-d, the IOMMU manages the access control policy between the hardware devices and some of the system critical units such as the memory. Configure the default Grub2 kernel command line to contain iommu=force as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) iommu=force" Depending on the hardware, devices and operating system used, enabling IOMMU can cause hardware instabilities. Proper function and stability should be assessed before applying remediation to production systems. R7 On x86 architectures, activating the I/OMMU prevents the system from arbitrary accesses potentially made by hardware devices. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "iommu" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"iommu=[^\"]*\"(.*]\s*)/\1\"iommu=force\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"iommu=force\"]" >> "$KARGS_DIR/10-iommu.toml" fi else grubby --update-kernel=ALL --args=iommu=force fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_enable_iommu_force - low_disruption - medium_complexity - reboot_required - restrict_strategy - unknown_severity - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="iommu=force" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_enable_iommu_force - low_disruption - medium_complexity - reboot_required - restrict_strategy - unknown_severity [customizations.kernel] append = "iommu=force" bootloader iommu=force Configure L1 Terminal Fault mitigations L1 Terminal Fault (L1TF) is a hardware vulnerability which allows unprivileged speculative access to data which is available in the Level 1 Data Cache when the page table entry isn't present. Select the appropriate mitigation by adding the argument l1tf= to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain l1tf= as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) l1tf=" Since Linux Kernel 4.19 you can check the L1TF vulnerability state with the following command: cat /sys/devices/system/cpu/vulnerabilities/l1tf Enabling L1TF mitigations may impact performance of the system. R8 The L1TF vulnerability allows an attacker to bypass memory access security controls imposed by the system or hypervisor. The L1TF vulnerability allows read access to any physical memory location that is cached in the L1 Data Cache. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then var_l1tf_options='' if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "l1tf" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"l1tf=[^\"]*\"(.*]\s*)/\1\"l1tf=$var_l1tf_options\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"l1tf=$var_l1tf_options\"]" >> "$KARGS_DIR/10-l1tf.toml" fi else grubby --update-kernel=ALL --args=l1tf=$var_l1tf_options fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_l1tf_argument - high_severity - low_disruption - medium_complexity - reboot_required - restrict_strategy - name: XCCDF Value var_l1tf_options # promote to variable set_fact: var_l1tf_options: !!str tags: - always - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="l1tf={{ var_l1tf_options }}" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_l1tf_argument - high_severity - low_disruption - medium_complexity - reboot_required - restrict_strategy [customizations.kernel] append = "l1tf=" bootloader l1tf= Force kernel panic on uncorrected MCEs A Machine Check Exception is an error generated by the CPU itdetects an error in itself, memory or I/O devices. These errors may be corrected and generate a check log entry, if an error cannot be corrected the kernel may panic or SIGBUS. To force the kernel to panic on any uncorrected error reported by Machine Check set the MCE tolerance to zero by adding mce=0 to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain mce=0 as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) mce=0" R8 Allowing uncorrected errors to result on a SIGBUS may allow an attacker to continue trying to exploit a vulnerability such as Rowhammer. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "mce" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"mce=[^\"]*\"(.*]\s*)/\1\"mce=0\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"mce=0\"]" >> "$KARGS_DIR/10-mce.toml" fi else grubby --update-kernel=ALL --args=mce=0 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_mce_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="mce=0" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_mce_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy [customizations.kernel] append = "mce=0" bootloader mce=0 Configure Microarchitectural Data Sampling mitigation Microarchitectural Data Sampling (MDS) is a hardware vulnerability which allows unprivileged speculative access to data which is available in various CPU internal buffers. When performing store, load, L1 refill operations, processors write data into temporary microarchitectural structures (buffers), and the data in the buffer can be forwarded to load operations as an optimization. Under certain conditions, data unrelated to the load operations can be speculatively forwarded from the buffers to a disclosure gadget which allows in turn to infer the value via a cache side channel attack. Select the appropriate mitigation by adding the argument mds= to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain mds= as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) mds=" Not all processors are affected by all variants of MDS, but the mitigation mechanism is identical for all of them. Since Linux Kernel 5.2 you can check whether the system is vulnerable or mitigated with the following command: cat /sys/devices/system/cpu/vulnerabilities/mds Enabling MDS mitigations will impact performance of the system, mainly by workloads with high rates of user-kernel-user space transitions. For example, system calls, NMIs and interrupts. R8 The MDS vulnerability allows an attacker to sample data from internal CPU buffers. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then var_mds_options='' if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "mds" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"mds=[^\"]*\"(.*]\s*)/\1\"mds=$var_mds_options\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"mds=$var_mds_options\"]" >> "$KARGS_DIR/10-mds.toml" fi else grubby --update-kernel=ALL --args=mds=$var_mds_options fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_mds_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: XCCDF Value var_mds_options # promote to variable set_fact: var_mds_options: !!str tags: - always - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="mds={{ var_mds_options }}" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_mds_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy [customizations.kernel] append = "mds=" bootloader mds= Ensure SMAP is not disabled during boot The SMAP is used to prevent the supervisor mode from unintentionally reading/writing into memory pages in the user space, it is enabled by default since Linux kernel 3.7. But it could be disabled through kernel boot parameters. Ensure that Supervisor Mode Access Prevention (SMAP) is not disabled by the nosmap boot paramenter option. Check that the line GRUB_CMDLINE_LINUX="..." within /etc/default/grub doesn't contain the argument nosmap. Run the following command to update command line for already installed kernels: # grubby --update-kernel=ALL --remove-args="nosmap" R1 Disabling SMAP can facilitate exploitation of vulnerabilities caused by unintended access and manipulation of data in the user space. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then sed -i -E "/kargs\s*=\s*\[\s*\"nosmap=[^\"]*\"\s*]/{:a;N;/^\n$/ba;N;/match-architectures.*/d;}" "$KARGS_DIR/*.toml" sed -i -E -e "s/^(\s*kargs\s*=\s*\[.*)\"nosmap=[^\"]*\"[,[:space:]]*(.*]\s*)/\1\2/" -e "s/^(\s*kargs.*),\s*\]$/\1\]/" "$KARGS_DIR/*.toml" else grubby --update-kernel=ALL --remove-args=nosmap fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_nosmap_argument_absent - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --remove-args="nosmap" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_nosmap_argument_absent - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy Ensure SMEP is not disabled during boot The SMEP is used to prevent the supervisor mode from executing user space code, it is enabled by default since Linux kernel 3.0. But it could be disabled through kernel boot parameters. Ensure that Supervisor Mode Execution Prevention (SMEP) is not disabled by the nosmep boot paramenter option. Check that the line GRUB_CMDLINE_LINUX="..." within /etc/default/grub doesn't contain the argument nosmep. Run the following command to update command line for already installed kernels: # grubby --update-kernel=ALL --remove-args="nosmep" R1 Disabling SMEP can facilitate exploitation of certain vulnerabilities because it allows the kernel to unintentionally execute code in less privileged memory space. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then sed -i -E "/kargs\s*=\s*\[\s*\"nosmep=[^\"]*\"\s*]/{:a;N;/^\n$/ba;N;/match-architectures.*/d;}" "$KARGS_DIR/*.toml" sed -i -E -e "s/^(\s*kargs\s*=\s*\[.*)\"nosmep=[^\"]*\"[,[:space:]]*(.*]\s*)/\1\2/" -e "s/^(\s*kargs.*),\s*\]$/\1\]/" "$KARGS_DIR/*.toml" else grubby --update-kernel=ALL --remove-args=nosmep fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_nosmep_argument_absent - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --remove-args="nosmep" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_nosmep_argument_absent - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy Enable Kernel Page-Table Isolation (KPTI) To enable Kernel page-table isolation, add the argument pti=on to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain pti=on as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) pti=on" SI-16 SRG-OS-000433-GPOS-00193 SRG-OS-000095-GPOS-00049 R8 Kernel page-table isolation is a kernel feature that mitigates the Meltdown security vulnerability and hardens the kernel against attempts to bypass kernel address space layout randomization (KASLR). # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "pti" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"pti=[^\"]*\"(.*]\s*)/\1\"pti=on\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"pti=on\"]" >> "$KARGS_DIR/10-pti.toml" fi else grubby --update-kernel=ALL --args=pti=on fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-SI-16 - grub2_pti_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="pti=on" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - NIST-800-53-SI-16 - grub2_pti_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy [customizations.kernel] append = "pti=on" bootloader pti=on Configure the confidence in TPM for entropy The TPM security chip that is available in most modern systems has a hardware RNG. It is also used to feed the entropy pool, but generally not credited entropy. Use rng_core.default_quality in the kernel command line to set the trust level on the hardware generators. The trust level defines the amount of entropy to credit. A value of 0 tells the system not to trust the hardware random number generators available, and doesn't credit any entropy to the pool. A value of 1000 assigns full confidence in the generators, and credits all the entropy it provides to the pool. Note that the value of rng_core.default_quality is global, affecting the trust on all hardware random number generators. Select the appropriate confidence by adding the argument rng_core.default_quality= to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain rng_core.default_quality= as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) rng_core.default_quality=" R8 A system may struggle to initialize its entropy pool and end up starving. Crediting entropy from the hardware number generators available in the system helps fill up the entropy pool. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then var_rng_core_default_quality='' if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "rng_core.default_quality" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"rng_core.default_quality=[^\"]*\"(.*]\s*)/\1\"rng_core.default_quality=$var_rng_core_default_quality\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"rng_core.default_quality=$var_rng_core_default_quality\"]" >> "$KARGS_DIR/10-rng_core_default_quality.toml" fi else grubby --update-kernel=ALL --args=rng_core.default_quality=$var_rng_core_default_quality fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_rng_core_default_quality_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy - name: XCCDF Value var_rng_core_default_quality # promote to variable set_fact: var_rng_core_default_quality: !!str tags: - always - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="rng_core.default_quality={{ var_rng_core_default_quality }}" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_rng_core_default_quality_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy [customizations.kernel] append = "rng_core.default_quality=" bootloader rng_core.default_quality= Disable merging of slabs with similar size The kernel may merge similar slabs together to reduce overhead and increase cache hotness of objects. Disabling merging of slabs keeps the slabs separate and reduces the risk of kernel heap overflows overwriting objects in merged caches. To disable merging of slabs in the Kernel add the argument slab_nomerge=yes to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain slab_nomerge=yes as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) slab_nomerge=yes" Disabling merge of slabs will slightly increase kernel memory utilization. R8 Disabling the merge of slabs of similar sizes prevents the kernel from merging a seemingly useless but vulnerable slab with a useful and valuable slab. This increase the risk that a heap overflow could overwrite objects from merged caches, with unmerged caches the heap overflow would only affect the objects in the same cache. Overall, this reduces the kernel attack surface area by isolating slabs from each other. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "slab_nomerge" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"slab_nomerge=[^\"]*\"(.*]\s*)/\1\"slab_nomerge=yes\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"slab_nomerge=yes\"]" >> "$KARGS_DIR/10-slab_nomerge.toml" fi else grubby --update-kernel=ALL --args=slab_nomerge=yes fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_slab_nomerge_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="slab_nomerge=yes" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_slab_nomerge_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy [customizations.kernel] append = "slab_nomerge=yes" bootloader slab_nomerge=yes Configure Speculative Store Bypass Mitigation Certain CPUs are vulnerable to an exploit against a common wide industry wide performance optimization known as Speculative Store Bypass (SSB). In such cases, recent stores to the same memory location cannot always be observed by later loads during speculative execution. However, such stores are unlikely and thus they can be detected prior to instruction retirement at the end of a particular speculation execution window. Since Linux Kernel 4.17 you can check the SSB mitigation state with the following command: cat /sys/devices/system/cpu/vulnerabilities/spec_store_bypass Select the appropriate SSB state by adding the argument spec_store_bypass_disable= to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain spec_store_bypass_disable= as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) spec_store_bypass_disable=" Disabling Speculative Store Bypass may impact performance of the system. R8 In vulnerable processsors, the speculatively forwarded store can be used in a cache side channel attack. An example of this is reading memory to which the attacker does not directly have access, for example inside the sandboxed code. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then var_spec_store_bypass_disable_options='' if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "spec_store_bypass_disable" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"spec_store_bypass_disable=[^\"]*\"(.*]\s*)/\1\"spec_store_bypass_disable=$var_spec_store_bypass_disable_options\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"spec_store_bypass_disable=$var_spec_store_bypass_disable_options\"]" >> "$KARGS_DIR/10-spec_store_bypass_disable.toml" fi else grubby --update-kernel=ALL --args=spec_store_bypass_disable=$var_spec_store_bypass_disable_options fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_spec_store_bypass_disable_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: XCCDF Value var_spec_store_bypass_disable_options # promote to variable set_fact: var_spec_store_bypass_disable_options: !!str tags: - always - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="spec_store_bypass_disable={{ var_spec_store_bypass_disable_options }}" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_spec_store_bypass_disable_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy [customizations.kernel] append = "spec_store_bypass_disable=" bootloader spec_store_bypass_disable= Enforce Spectre v2 mitigation Spectre V2 is an indirect branch poisoning attack that can lead to data leakage. An exploit for Spectre V2 tricks the indirect branch predictor into executing code from a future indirect branch chosen by the attacker, even if the privilege level is different. Since Linux Kernel 4.15 you can check the Spectre V2 mitigation state with the following command: cat /sys/devices/system/cpu/vulnerabilities/spectre_v2 Enforce the Spectre V2 mitigation by adding the argument spectre_v2=on to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain spectre_v2=on as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) spectre_v2=on" R8 The Spectre V2 vulnerability allows an attacker to read memory that he should not have access to. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "spectre_v2" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"spectre_v2=[^\"]*\"(.*]\s*)/\1\"spectre_v2=on\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"spectre_v2=on\"]" >> "$KARGS_DIR/10-spectre_v2.toml" fi else grubby --update-kernel=ALL --args=spectre_v2=on fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_spectre_v2_argument - high_severity - low_disruption - medium_complexity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="spectre_v2=on" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_spectre_v2_argument - high_severity - low_disruption - medium_complexity - reboot_required - restrict_strategy [customizations.kernel] append = "spectre_v2=on" bootloader spectre_v2=on Ensure debug-shell service is not enabled during boot systemd's debug-shell service is intended to diagnose systemd related boot issues with various systemctl commands. Once enabled and following a system reboot, the root shell will be available on tty9 which is access by pressing CTRL-ALT-F9. The debug-shell service should only be used for systemd related issues and should otherwise be disabled. By default, the debug-shell systemd service is already disabled. Ensure the debug-shell is not enabled by the systemd.debug-shel=1 boot parameter option. Check that the line GRUB_CMDLINE_LINUX="..." within /etc/default/grub doesn't contain the argument systemd.debug-shell. Run the following command to update command line for already installed kernels: # grubby --update-kernel=ALL --remove-args="systemd.debug-shell" FIA_UAU.1 This prevents attackers with physical access from trivially bypassing security on the machine through valid troubleshooting configurations and gaining root access when the system is rebooted. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then sed -i -E "/kargs\s*=\s*\[\s*\"systemd.debug-shell=[^\"]*\"\s*]/{:a;N;/^\n$/ba;N;/match-architectures.*/d;}" "$KARGS_DIR/*.toml" sed -i -E -e "s/^(\s*kargs\s*=\s*\[.*)\"systemd.debug-shell=[^\"]*\"[,[:space:]]*(.*]\s*)/\1\2/" -e "s/^(\s*kargs.*),\s*\]$/\1\]/" "$KARGS_DIR/*.toml" else grubby --update-kernel=ALL --remove-args=systemd.debug-shell fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - grub2_systemd_debug-shell_argument_absent - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --remove-args="systemd.debug-shell" when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - grub2_systemd_debug-shell_argument_absent - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy Disable vsyscalls To disable use of virtual syscalls, add the argument vsyscall=none to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain vsyscall=none as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) vsyscall=none" The vsyscall emulation is only available on x86_64 architecture (CONFIG_X86_VSYSCALL_EMULATION) making this rule not applicable to other CPU architectures. CM-7(a) FPT_ASLR_EXT.1 SRG-OS-000480-GPOS-00227 SRG-OS-000134-GPOS-00068 Virtual Syscalls provide an opportunity of attack for a user who has control of the return instruction pointer. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ) && { ( grep -sqE "^.*\.x86_64$" /proc/sys/kernel/osrelease || grep -sqE "^x86_64$" /proc/sys/kernel/arch; ); }; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "vsyscall" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"vsyscall=[^\"]*\"(.*]\s*)/\1\"vsyscall=none\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"vsyscall=none\"]" >> "$KARGS_DIR/10-vsyscall.toml" fi else grubby --update-kernel=ALL --args=vsyscall=none fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-7(a) - grub2_vsyscall_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="vsyscall=none" when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - ansible_architecture == "x86_64" tags: - NIST-800-53-CM-7(a) - grub2_vsyscall_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy [customizations.kernel] append = "vsyscall=none" bootloader vsyscall=none Non-UEFI GRUB2 bootloader configuration Non-UEFI GRUB2 bootloader configuration Verify /boot/grub2/grub.cfg Group Ownership The file /boot/grub2/grub.cfg should be group-owned by the root group to prevent destruction or modification of the file. To properly set the group owner of /boot/grub2/grub.cfg, run the command: $ sudo chgrp root /boot/grub2/grub.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 SRG-OS-000480-GPOS-00227 R29 1.4.2 2.2.6 2.2 The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ) && { ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ); }; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/boot/grub2/grub.cfg" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /boot/grub2/grub.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_grub2_cfg_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_grub2_cfg_newgroup: '0' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/grub.cfg ansible.builtin.stat: path: /boot/grub2/grub.cfg register: file_exists when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /boot/grub2/grub.cfg ansible.builtin.file: path: /boot/grub2/grub.cfg follow: false group: '{{ file_groupowner_grub2_cfg_newgroup }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/user.cfg Group Ownership The file /boot/grub2/user.cfg should be group-owned by the root group to prevent reading or modification of the file. To properly set the group owner of /boot/grub2/user.cfg, run the command: $ sudo chgrp root /boot/grub2/user.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 SRG-OS-000480-GPOS-00227 R29 1.4.2 2.2.6 2.2 The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway. Non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ) && { ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ); }; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/boot/grub2/user.cfg" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /boot/grub2/user.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_user_cfg_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_user_cfg_newgroup: '0' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/user.cfg ansible.builtin.stat: path: /boot/grub2/user.cfg register: file_exists when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /boot/grub2/user.cfg ansible.builtin.file: path: /boot/grub2/user.cfg follow: false group: '{{ file_groupowner_user_cfg_newgroup }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/grub.cfg User Ownership The file /boot/grub2/grub.cfg should be owned by the root user to prevent destruction or modification of the file. To properly set the owner of /boot/grub2/grub.cfg, run the command: $ sudo chown root /boot/grub2/grub.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 SRG-OS-000480-GPOS-00227 R29 1.4.2 2.2.6 2.2 Only root should be able to modify important boot parameters. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ) && { ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ); }; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/boot/grub2/grub.cfg" | grep -E -w -q "0"; then chown --no-dereference "$newown" /boot/grub2/grub.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_grub2_cfg_newown variable if represented by uid ansible.builtin.set_fact: file_owner_grub2_cfg_newown: '0' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/grub.cfg ansible.builtin.stat: path: /boot/grub2/grub.cfg register: file_exists when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /boot/grub2/grub.cfg ansible.builtin.file: path: /boot/grub2/grub.cfg follow: false owner: '{{ file_owner_grub2_cfg_newown }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/user.cfg User Ownership The file /boot/grub2/user.cfg should be owned by the root user to prevent reading or modification of the file. To properly set the owner of /boot/grub2/user.cfg, run the command: $ sudo chown root /boot/grub2/user.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 R29 1.4.2 2.2.6 2.2 Only root should be able to modify important boot parameters. Also, non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ) && { ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ); }; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/boot/grub2/user.cfg" | grep -E -w -q "0"; then chown --no-dereference "$newown" /boot/grub2/user.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_user_cfg_newown variable if represented by uid ansible.builtin.set_fact: file_owner_user_cfg_newown: '0' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/user.cfg ansible.builtin.stat: path: /boot/grub2/user.cfg register: file_exists when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /boot/grub2/user.cfg ansible.builtin.file: path: /boot/grub2/user.cfg follow: false owner: '{{ file_owner_user_cfg_newown }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/grub.cfg Permissions File permissions for /boot/grub2/grub.cfg should be set to 600. To properly set the permissions of /boot/grub2/grub.cfg, run the command: $ sudo chmod 600 /boot/grub2/grub.cfg 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 R29 1.4.2 2.2.6 2.2 Proper permissions ensure that only the root user can modify important boot parameters. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ) && { ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ); }; then chmod u-xs,g-xwrs,o-xwrt /boot/grub2/grub.cfg else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/grub.cfg ansible.builtin.stat: path: /boot/grub2/grub.cfg register: file_exists when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xwrs,o-xwrt on /boot/grub2/grub.cfg ansible.builtin.file: path: /boot/grub2/grub.cfg mode: u-xs,g-xwrs,o-xwrt when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/user.cfg Permissions File permissions for /boot/grub2/user.cfg should be set to 600. To properly set the permissions of /boot/grub2/user.cfg, run the command: $ sudo chmod 600 /boot/grub2/user.cfg 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 R29 1.4.2 2.2.6 2.2 Proper permissions ensure that only the root user can read or modify important boot parameters. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ) && { ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ); }; then chmod u-xs,g-xwrs,o-xwrt /boot/grub2/user.cfg else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/user.cfg ansible.builtin.stat: path: /boot/grub2/user.cfg register: file_exists when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xwrs,o-xwrt on /boot/grub2/user.cfg ansible.builtin.file: path: /boot/grub2/user.cfg mode: u-xs,g-xwrs,o-xwrt when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Set the Boot Loader Admin Username to a Non-Default Value The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings. To maximize the protection, select a password-protected superuser account with unique name, and modify the /etc/grub.d/01_users configuration file to reflect the account name change. Do not to use common administrator account names like root, admin, or administrator for the grub2 superuser account. Change the superuser to a different username (The default is 'root'). $ sed -i 's/\(set superusers=\).*/\1"<unique user ID>"/g' /etc/grub.d/01_users The line mentioned above must be followed by the line export superusers so that the superusers is honored. Once the superuser account has been added, update the grub.cfg file by running: grubby --update-kernel=ALL To prevent hard-coded admin usernames, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file. 1 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.06 DSS06.10 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CM-6(a) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.PT-3 SRG-OS-000080-GPOS-00048 Having a non-default grub superuser username makes password-guessing attacks less effective. Set Boot Loader Password in grub2 The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings. Since plaintext passwords are a security risk, generate a hash for the password by running the following command: # grub2-setpassword When prompted, enter the password that was selected. To prevent hard-coded passwords, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file. 1 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.06 DSS06.10 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.18.1.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CM-6(a) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.PT-3 FIA_UAU.1 SRG-OS-000080-GPOS-00048 R5 1.4.1 Password protection on the boot loader configuration ensures users with physical access cannot trivially alter important bootloader settings. These include which kernel to use, and whether to enter single-user mode. UEFI GRUB2 bootloader configuration UEFI GRUB2 bootloader configuration UEFI generally uses vfat file systems, which does not support Unix-style permissions managed by chmod command. In this case, in order to change file permissions for files within /boot/efi it is necessary to update the mount options in /etc/fstab file and reboot the system. Verify the UEFI Boot Loader grub.cfg Group Ownership The file /boot/grub2/grub.cfg should be group-owned by the root group to prevent destruction or modification of the file. To properly set the group owner of /boot/grub2/grub.cfg, run the command: $ sudo chgrp root /boot/grub2/grub.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 R29 The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/boot/grub2/grub.cfg" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /boot/grub2/grub.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_efi_grub2_cfg_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_efi_grub2_cfg_newgroup: '0' when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/grub.cfg ansible.builtin.stat: path: /boot/grub2/grub.cfg register: file_exists when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /boot/grub2/grub.cfg ansible.builtin.file: path: /boot/grub2/grub.cfg follow: false group: '{{ file_groupowner_efi_grub2_cfg_newgroup }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/user.cfg Group Ownership The file /boot/grub2/user.cfg should be group-owned by the root group to prevent reading or modification of the file. To properly set the group owner of /boot/grub2/user.cfg, run the command: $ sudo chgrp root /boot/grub2/user.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 R29 The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway. Non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/boot/grub2/user.cfg" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /boot/grub2/user.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_efi_user_cfg_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_efi_user_cfg_newgroup: '0' when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/user.cfg ansible.builtin.stat: path: /boot/grub2/user.cfg register: file_exists when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /boot/grub2/user.cfg ansible.builtin.file: path: /boot/grub2/user.cfg follow: false group: '{{ file_groupowner_efi_user_cfg_newgroup }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_groupowner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify the UEFI Boot Loader grub.cfg User Ownership The file /boot/grub2/grub.cfg should be owned by the root user to prevent destruction or modification of the file. To properly set the owner of /boot/grub2/grub.cfg, run the command: $ sudo chown root /boot/grub2/grub.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 R29 Only root should be able to modify important boot parameters. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/boot/grub2/grub.cfg" | grep -E -w -q "0"; then chown --no-dereference "$newown" /boot/grub2/grub.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_efi_grub2_cfg_newown variable if represented by uid ansible.builtin.set_fact: file_owner_efi_grub2_cfg_newown: '0' when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/grub.cfg ansible.builtin.stat: path: /boot/grub2/grub.cfg register: file_exists when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /boot/grub2/grub.cfg ansible.builtin.file: path: /boot/grub2/grub.cfg follow: false owner: '{{ file_owner_efi_grub2_cfg_newown }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/user.cfg User Ownership The file /boot/grub2/user.cfg should be owned by the root user to prevent reading or modification of the file. To properly set the owner of /boot/grub2/user.cfg, run the command: $ sudo chown root /boot/grub2/user.cfg 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-7.1 R29 Only root should be able to modify important boot parameters. Also, non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/boot/grub2/user.cfg" | grep -E -w -q "0"; then chown --no-dereference "$newown" /boot/grub2/user.cfg fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_efi_user_cfg_newown variable if represented by uid ansible.builtin.set_fact: file_owner_efi_user_cfg_newown: '0' when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/user.cfg ansible.builtin.stat: path: /boot/grub2/user.cfg register: file_exists when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /boot/grub2/user.cfg ansible.builtin.file: path: /boot/grub2/user.cfg follow: false owner: '{{ file_owner_efi_user_cfg_newown }}' when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-7.1 - configure_strategy - file_owner_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify the UEFI Boot Loader grub.cfg Permissions File permissions for /boot/grub2/grub.cfg should be set to 700. To properly set the permissions of /boot/grub2/grub.cfg, run the command: $ sudo chmod 700 /boot/grub2/grub.cfg 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 R29 Proper permissions ensure that only the root user can modify important boot parameters. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then chmod u-s,g-xwrs,o-xwrt /boot/grub2/grub.cfg else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/grub.cfg ansible.builtin.stat: path: /boot/grub2/grub.cfg register: file_exists when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-xwrs,o-xwrt on /boot/grub2/grub.cfg ansible.builtin.file: path: /boot/grub2/grub.cfg mode: u-s,g-xwrs,o-xwrt when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_efi_grub2_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify /boot/grub2/user.cfg Permissions File permissions for /boot/grub2/user.cfg should be set to 600. To properly set the permissions of /boot/grub2/user.cfg, run the command: $ sudo chmod 600 /boot/grub2/user.cfg 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.4.5 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 R29 Proper permissions ensure that only the root user can read or modify important boot parameters. # Remediation is applicable only in certain platforms if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel ); then chmod u-s,g-xwrs,o-xwrt /boot/grub2/user.cfg else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /boot/grub2/user.cfg ansible.builtin.stat: path: /boot/grub2/user.cfg register: file_exists when: ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-xwrs,o-xwrt on /boot/grub2/user.cfg ansible.builtin.file: path: /boot/grub2/user.cfg mode: u-s,g-xwrs,o-xwrt when: - ( "grub2-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-171-3.4.5 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_efi_user_cfg - low_complexity - low_disruption - medium_severity - no_reboot_needed Set the UEFI Boot Loader Admin Username to a Non-Default Value The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings. To maximize the protection, select a password-protected superuser account with unique name, and modify the /etc/grub.d/01_users configuration file to reflect the account name change. It is highly suggested not to use common administrator account names like root, admin, or administrator for the grub2 superuser account. Change the superuser to a different username (The default is 'root'). $ sed -i 's/\(set superusers=\).*/\1"<unique user ID>"/g' /etc/grub.d/01_users The line mentioned above must be followed by the line export superusers so that the superusers is honored. Once the superuser account has been added, update the grub.cfg file by running: grubby --update-kernel=ALL To prevent hard-coded admin usernames, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file. 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) PR.AC-4 PR.AC-6 PR.PT-3 SRG-OS-000080-GPOS-00048 Having a non-default grub superuser username makes password-guessing attacks less effective. Set the UEFI Boot Loader Password The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings. Since plaintext passwords are a security risk, generate a hash for the password by running the following command: # grub2-setpassword When prompted, enter the password that was selected. To prevent hard-coded passwords, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file. 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 3.4.5 164.308(a)(1)(ii)(B) 164.308(a)(7)(i) 164.308(a)(7)(ii)(A) 164.310(a)(1) 164.310(a)(2)(i) 164.310(a)(2)(ii) 164.310(a)(2)(iii) 164.310(b) 164.310(c) 164.310(d)(1) 164.310(d)(2)(iii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) PR.AC-4 PR.AC-6 PR.PT-3 FIA_UAU.1 SRG-OS-000080-GPOS-00048 R5 Password protection on the boot loader configuration ensures users with physical access cannot trivially alter important bootloader settings. These include which kernel to use, and whether to enter single-user mode. Protect Random-Number Entropy Pool The I/O operations of the Linux kernel block layer due to their inherently unpredictable execution times have been traditionally considered as a reliable source to contribute to random-number entropy pool of the Linux kernel. This has changed with introduction of solid-state storage devices (SSDs) though. Ensure Solid State Drives Do Not Contribute To Random-Number Entropy Pool For each solid-state drive on the system, run: # echo 0 > /sys/block/DRIVE/queue/add_random In contrast to traditional electromechanical magnetic disks, containing spinning disks and / or movable read / write heads, the solid-state storage devices (SSDs) do not contain moving / mechanical components. Therefore the I/O operation completion times are much more predictable for them. Kernel Configuration Contains rules that check the kernel configuration that was used to build it. Hash function for kernel module signing The hash function to use when signing modules during kernel build process. sha512 sha1 sha224 sha256 sha384 sha512 Key and certificate for kernel module signing The private key and certificate to use when signing modules during kernel build process. On systems where the OpenSSL ENGINE_pkcs11 is functional — a PKCS#11 URI as defined by RFC7512 In the latter case, the PKCS#11 URI should reference both a certificate and a private key. certs/signing_key.pem certs/signing_key.pem Kernel panic timeout The time, in seconds, to wait until a reboot occurs. If the value is 0 the system never reboots. If the value is less than 0 the system reboots immediately. 0 0 300 60 -1 Do not allow ACPI methods to be inserted/replaced at run time This debug facility allows ACPI AML methods to be inserted and/or replaced without rebooting the system. This configuration is available from kernel 3.0. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_ACPI_CUSTOM_METHOD, run the following command: grep CONFIG_ACPI_CUSTOM_METHOD /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 Enabling this feature allows arbitrary kernel memory to be written to by root (uid=0) users, allowing them to bypass certain security measures Emulate Privileged Access Never (PAN) Enabling this option prevents the kernel from accessing user-space memory directly by pointing TTBR0_EL1 to a reserved zeroed area and reserved ASID. The user access routines restore the valid TTBR0_EL1 temporarily. This configuration is available from kernel 4.10, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_ARM64_SW_TTBR0_PAN, run the following command: grep CONFIG_ARM64_SW_TTBR0_PAN /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R27 The Privileged Access Never (PAN) is the ARM equivalent of the x86 Supervisor Mode Access Prevention (SMAP), and it prevents privileged acccess to user data unless explicitly enabled. Disable kernel support for MISC binaries Enabling CONFIG_BINFMT_MISC makes it possible to plug wrapper-driven binary formats into the kernel. This is specially useful for programs that need an interpreter to run like Java, Python and DOS emulators. Once you have registered such a binary class with the kernel, you can start one of those programs simply by typing in its name at a shell prompt. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_BINFMT_MISC, run the following command: grep CONFIG_BINFMT_MISC /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R23 This disables arbitrary binary format support and helps reduce attack surface. Enable support for BUG() Disabling this option eliminates support for BUG and WARN, reducing the size of your kernel image and potentially quietly ignoring numerous fatal conditions. You should only consider disabling this option for embedded systems with no facilities for reporting errors. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_BUG, run the following command: grep CONFIG_BUG /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R19 Not setting this variable may hide a number of critical errors. Trigger a kernel BUG when data corruption is detected This option makes the kernel BUG when it encounters data corruption in kernel memory structures when they get checked for validity. This configuration is available from kernel 4.10. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_BUG_ON_DATA_CORRUPTION, run the following command: grep CONFIG_BUG_ON_DATA_CORRUPTION /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R16 This helps detect data corruptions early and stop with a BUG() error message. Disable compatibility with brk() Enabling compatiliby with brk() allows legacy binaries to run (i.e. those linked against libc5). But this compatibility comes at the cost of not being able to randomize the heap placement (ASLR). Unless legacy binaries need to run on the system, set CONFIG_COMPAT_BRK to "n". The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_COMPAT_BRK, run the following command: grep CONFIG_COMPAT_BRK /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 Enabling compatibility with brk() disables support for ASLR. Disable the 32-bit vDSO Certain buggy versions of glibc (2.3.3) will crash if they are presented with a 32-bit vDSO that is not mapped at the address indicated in its segment table. Setting CONFIG_COMPAT_VDSO to y turns off the 32-bit VDSO and works aroud the glibc bug. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_COMPAT_VDSO, run the following command: grep CONFIG_COMPAT_VDSO /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 Enabling VDSO compatibility hurts performance and disables ASLR. Enable checks on credential management Enable this to turn on some debug checking for credential management. The additional code keeps track of the number of pointers from task_structs to any given cred struct, and checks to see that this number never exceeds the usage count of the cred struct. Furthermore, if SELinux is enabled, this also checks that the security pointer in the cred struct is never seen to be invalid. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEBUG_CREDENTIALS, run the following command: grep CONFIG_DEBUG_CREDENTIALS /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R16 This adds sanity checks and validations to credential data structures. Disable kernel debugfs debugfs is a virtual file system that kernel developers use to put debugging files into. Enable this option to be able to read and write to these files. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEBUG_FS, run the following command: grep CONFIG_DEBUG_FS /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 To reduce the attack surface, this file system should be disabled if not in use. Enable checks on linked list manipulation Enable this to turn on extended checks in the linked-list walking routines. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEBUG_LIST, run the following command: grep CONFIG_DEBUG_LIST /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R16 This add sanity checks to manipulation of linked lists structures in the kernel and may prevent exploits such as CVE-2017-1661, where a race condition and simultaneos operations caused a list to corrupt. Enable checks on notifier call chains Enable this to turn on sanity checking for notifier call chains. This is most useful for kernel developers to make sure that modules properly unregister themselves from notifier chains. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEBUG_NOTIFIERS, run the following command: grep CONFIG_DEBUG_NOTIFIERS /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R16 This provides validation of notifier chains, it checks whether the notifiers are from the kernel or a module that is still loaded prior to being invoked. Enable checks on scatter-gather (SG) table operations Scatter-gather tables are mechanism used for high performance I/O on DMA devices. Enable this to turn on checks on scatter-gather tables. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEBUG_SG, run the following command: grep CONFIG_DEBUG_SG /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R16 This can help find problems with drivers that do not properly initialize their SG tables. Warn on W+X mappings found at boot Generate a warning if any W+X mappings are found at boot. This configuration is available from kernel 5.8. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEBUG_WX, run the following command: grep CONFIG_DEBUG_WX /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This is useful for discovering cases where the kernel is leaving W+X mappings after applying NX, as such mappings are a security risk. Note that even if the check fails, your kernel is possibly still fine, as W+X mappings are not a security hole in themselves, what they do is that they make the exploitation of other unfixed kernel bugs easier. Configure Low Address Space To Protect From User Allocation This is the portion of low virtual memory which should be protected from userspace allocation. This configuration is available from kernel 3.14, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEFAULT_MMAP_MIN_ADDR, run the following command: grep CONFIG_DEFAULT_MMAP_MIN_ADDR /boot/config-* For each kernel installed, a line with value should be returned. If the system architecture is x86_64, the value should be 65536. If the system architecture is aarch64, the value should be 32768. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R25 R27 Keeping a user from writing to low pages can help reduce the impact of kernel NULL pointer bugs. Disable /dev/kmem virtual device support Disable support for the /dev/kmem device. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_DEVKMEM, run the following command: grep CONFIG_DEVKMEM /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 The /dev/kmem device is rarely used, but can be used for certain kind of kernel debugging operations. Harden common str/mem functions against buffer overflows Detect overflows of buffers in common string and memory functions where the compiler can determine and validate the buffer sizes. This configuration is available from kernel 4.13, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_FORTIFY_SOURCE, run the following command: grep CONFIG_FORTIFY_SOURCE /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This features helps reduce likelihood of memory corruption of kernel structures. Harden memory copies between kernel and userspace This option checks for obviously wrong memory regions when copying memory to/from the kernel (via copy_to_user() and copy_from_user() functions) by rejecting memory ranges that are larger than the specified heap object, span multiple separately allocated pages, are not on the process stack, or are part of the kernel text. This configuration is available from kernel 4.8, and may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_HARDENED_USERCOPY, run the following command: grep CONFIG_HARDENED_USERCOPY /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This config prevents entire classes of heap overflow exploits and similar kernel memory exposures. Do not allow usercopy whitelist violations to fallback to object size This is a temporary option that allows missing usercopy whitelists to be discovered via a WARN() to the kernel log, instead of rejecting the copy, falling back to non-whitelisted hardened usercopy that checks the slab allocation size instead of the whitelist size. This configuration is available from kernel 4.16. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_HARDENED_USERCOPY_FALLBACK, run the following command: grep CONFIG_HARDENED_USERCOPY_FALLBACK /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This config prevents entire classes of heap overflow exploits and similar kernel memory exposures. Disable hibernation Enable the suspend to disk (STD) functionality, which is usually called "hibernation" in user interfaces. STD checkpoints the system and powers it off; and restores that checkpoint on reboot. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_HIBERNATION, run the following command: grep CONFIG_HIBERNATION /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R23 Suspending to disk allows one to replace the running kernel. Disable IA32 emulation Disables support for legacy 32-bit programs under a 64-bit kernel. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_IA32_EMULATION, run the following command: grep CONFIG_IA32_EMULATION /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. Only disable support for 32-bit programs if you are sure you don't need any 32-bit program. R25 Disabling 32-bit backwards compatibility helps reduce the attack surface. Disable the IPv6 protocol Disable support for IP version 6 (IPv6). The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_IPV6, run the following command: grep CONFIG_IPV6 /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. Any unnecessary network stacks, including IPv6, should be disabled to reduce the vulnerability to exploitation. Disable kexec system call kexec is a system call that implements the ability to shutdown your current kernel, and to start another kernel. It is like a reboot but it is independent of the system firmware. And like a reboot you can start any kernel with it, not just Linux. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_KEXEC, run the following command: grep CONFIG_KEXEC /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R23 Prohibits the execution of a new kernel image after reboot. Disable legacy (BSD) PTY support Disable the Linux traditional BSD-like terminal names /dev/ptyxx for masters and /dev/ttyxx for slaves of pseudo terminals, and use only the modern ptys (devpts) interface. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_LEGACY_PTYS, run the following command: grep CONFIG_LEGACY_PTYS /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R23 The legacy scheme has a number of security problems. Disable vsyscall emulation The kernel traps and emulates calls into the fixed vsyscall address mapping. This configuration is available from kernel 5.3, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_LEGACY_VSYSCALL_EMULATE, run the following command: grep CONFIG_LEGACY_VSYSCALL_EMULATE /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 The mapping is non-executable, but it still contains known contents, which could be used in certain rare security vulnerability exploits. Disable vsyscall mapping This config disables the vsyscall mapping at all. Attempts to use the vsyscalls will be reported to dmesg, so that either old or malicious userspace programs can be identified. This configuration is available from kernel 4.4. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_LEGACY_VSYSCALL_NONE, run the following command: grep CONFIG_LEGACY_VSYSCALL_NONE /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This will eliminate any risk of ASLR bypass due to the vsyscall fixed address mapping. Disable vsyscall emulate execution only The kernel traps and emulates calls into the fixed vsyscall address mapping and does not allow reads. This configuration is available from kernel 5.3. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_LEGACY_VSYSCALL_XONLY, run the following command: grep CONFIG_LEGACY_VSYSCALL_XONLY /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 Disabling this mitigates certain uses of the vsyscall area as an ASLR-bypassing buffer. Disable the LDT (local descriptor table) Linux can allow user programs to install a per-process x86 Local Descriptor Table (LDT) using the modify_ldt(2) system call. This is required to run 16-bit or segmented code such as DOSEMU or some Wine programs. It is also used by some very old threading libraries. This configuration is available from kernel 4.3, but may be available if backported by distros. Disable LDT if 16-bit program emulation is not necessary. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_MODIFY_LDT_SYSCALL, run the following command: grep CONFIG_MODIFY_LDT_SYSCALL /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R25 Disabling support for unnecessary code reduces attack surface. Enable module signature verification Check modules for valid signatures upon load. Note that this option adds the OpenSSL development packages as a kernel build dependency so that the signing tool can use its crypto library. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_MODULE_SIG, run the following command: grep CONFIG_MODULE_SIG /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R18 Loaded modules must be signed. Enable automatic signing of all modules Sign all modules during make modules_install. Without this option, modules must be signed manually, using the scripts/sign-file tool. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_MODULE_SIG_ALL, run the following command: grep CONFIG_MODULE_SIG_ALL /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R18 This ensures the modules are signed during install process. Require modules to be validly signed Reject unsigned modules or signed modules with an unknown key. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_MODULE_SIG_FORCE, run the following command: grep CONFIG_MODULE_SIG_FORCE /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R18 Prevent loading modules that are unsigned or signed with an unknown key. Specify the hash to use when signing modules This configures the kernel to build and sign modules using as the hash function. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_MODULE_SIG_HASH, run the following command: grep CONFIG_MODULE_SIG_HASH /boot/config-* For each kernel installed, a line with value "" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R18 Use of strong hash function is important to secure the module against counterfeit signatures. Specify module signing key to use Setting this option to something other than its default of certs/signing_key.pem will disable the autogeneration of signing keys and allow the kernel modules to be signed with a key of your choosing. The string provided should identify a file containing both a private key and its corresponding X.509 certificate in PEM form, or — on systems where the OpenSSL ENGINE_pkcs11 is functional — a PKCS#11 URI as defined by RFC7512. In the latter case, the PKCS#11 URI should reference both a certificate and a private key. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_MODULE_SIG_KEY, run the following command: grep CONFIG_MODULE_SIG_KEY /boot/config-* For each kernel installed, a line with value "" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R18 A key and certificate is required to sign the built modules. Sign kernel modules with SHA-512 This configures the kernel to build and sign modules using SHA512 as the hash function. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_MODULE_SIG_SHA512, run the following command: grep CONFIG_MODULE_SIG_SHA512 /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R18 Use of strong hash function is important to secure the module against counterfeit signatures. Enable poison of pages after freeing Fill the pages with poison patterns after free_pages() and verify the patterns before alloc_pages. This does have a potential performance impact if enabled with the "page_poison=1" kernel boot option. This configuration is available from kernel 4.6. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_PAGE_POISONING, run the following command: grep CONFIG_PAGE_POISONING /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 The filling of the memory helps reduce the risk of information leaks from freed data. Enable poison without sanity check Skip the sanity checking on alloc, only fill the pages with poison on free. This reduces some of the overhead of the poisoning feature. This configuration is available from kernel 4.6. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_PAGE_POISONING_NO_SANITY, run the following command: grep CONFIG_PAGE_POISONING_NO_SANITY /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 This configuration helps alleviates the performance impact of poisonining. Use zero for poisoning instead of debugging value Instead of using the existing poison value, fill the pages with zeros. This makes it harder to detect when errors are occurring due to sanitization but the zeroing at free means that it is no longer necessary to write zeros when GFP_ZERO is used on allocation. This configuration is available from kernel 4.19. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_PAGE_POISONING_ZERO, run the following command: grep CONFIG_PAGE_POISONING_ZERO /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 This configuration helps alleviates the performance impact of poisonining. Remove the kernel mapping in user mode This feature reduces the number of hardware side channels by ensuring that the majority of kernel addresses are not mapped into userspace. This configuration is available from kernel 4.15, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_PAGE_TABLE_ISOLATION, run the following command: grep CONFIG_PAGE_TABLE_ISOLATION /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R25 This is a countermeasure to the Meltdown attack. Kernel panic oops Enable the kernel to panic when it oopses. This has the same effect as setting oops=panic on the kernel command line. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_PANIC_ON_OOPS, run the following command: grep CONFIG_PANIC_ON_OOPS /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R19 This feature ensures that the kernel does not do anything erroneous after an oops which could result in data corruption or other issues. Kernel panic timeout Set the timeout value (in seconds) until a reboot occurs when the kernel panics. A timeout of 0 configures the system to wait forever. With a timeout value greater than 0, the system will wait the specified amount of seconds before rebooting. While a timeout value less than 0 makes the system reboot immediately. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_PANIC_TIMEOUT, run the following command: grep CONFIG_PANIC_TIMEOUT /boot/config-* For each kernel installed, a line with value "" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R19 This is required to enable protection against Spectre v2. Disable support for /proc/kkcore Provides a virtual ELF core file of the live kernel. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_PROC_KCORE, run the following command: grep CONFIG_PROC_KCORE /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This feature exposes the memory to the userspace and can assist an attacker in discovering attack vectors. Randomize the address of the kernel image (KASLR) In support of Kernel Address Space Layout Randomization (KASLR), this randomizes the physical address at which the kernel image is decompressed and the virtual address where the kernel image is mapped. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_RANDOMIZE_BASE, run the following command: grep CONFIG_RANDOMIZE_BASE /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R25 R27 An unpredictable kernel address makes it more difficult to succeed with exploits that rely on knowledge of the location of kernel code internals. Randomize the kernel memory sections Randomizes the base virtual address of kernel memory sections (physical memory mapping, vmalloc & vmemmap). This configuration is available from kernel 4.8, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_RANDOMIZE_MEMORY, run the following command: grep CONFIG_RANDOMIZE_MEMORY /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R25 This security feature makes exploits relying on predictable memory locations less reliable. Perform full reference count validation Enabling this switches the refcounting infrastructure from a fast unchecked atomic_t implementation to a fully state checked implementation, which can have a slight impact in performance. This configuration is available from kernel 4.13, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_REFCOUNT_FULL, run the following command: grep CONFIG_REFCOUNT_FULL /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 Refcounting provides protections against various use-after-free conditions that can be used in security flaw exploits. Avoid speculative indirect branches in kernel Compile kernel with the retpoline compiler options to guard against kernel-to-user data leaks by avoiding speculative indirect branches. Requires a compiler with -mindirect-branch=thunk-extern support for full protection. The kernel may run slower. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_RETPOLINE, run the following command: grep CONFIG_RETPOLINE /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This is required to enable protection against Spectre v2. Detect stack corruption on calls to schedule() This option checks for a stack overrun on calls to schedule(). If the stack end location is found to be overwritten always panic as the content of the corrupted region can no longer be trusted. This configuration is available from kernel 3.18. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SCHED_STACK_END_CHECK, run the following command: grep CONFIG_SCHED_STACK_END_CHECK /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This ensures no erroneous behaviour occurs which could result in data corruption or a sporadic crash at a later stage once the region is examined. Enable seccomp to safely compute untrusted bytecode This kernel feature is useful for number crunching applications that may need to compute untrusted bytecode during their execution. By using pipes or other transports made available to the process as file descriptors supporting the read/write syscalls, it's possible to isolate those applications in their own address space using seccomp. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SECCOMP, run the following command: grep CONFIG_SECCOMP /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R20 seccomp enables the ability to filter system calls made by an application, effectively isolating the system's resources from it. Enable use of Berkeley Packet Filter with seccomp Enable tasks to build secure computing environments defined in terms of Berkeley Packet Filter programs which implement task-defined system call filtering polices. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SECCOMP_FILTER, run the following command: grep CONFIG_SECCOMP_FILTER /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R20 Use of BPF filters allows for expressive filtering of system calls using a filter program language with a long history of being exposed to userland. Enable different security models This allows you to choose different security modules to be configured into your kernel. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SECURITY, run the following command: grep CONFIG_SECURITY /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R20 This is enables kernel security primitives required by the LSM framework. Restrict unprivileged access to the kernel syslog Enforce restrictions on unprivileged users reading the kernel syslog via dmesg(8). The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SECURITY_DMESG_RESTRICT, run the following command: grep CONFIG_SECURITY_DMESG_RESTRICT /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 Prevents unprivileged users from retrieving kernel addresses with dmesg. Disable mutable hooks Ensure kernel structures associated with LSMs are always mapped as read-only after system boot. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SECURITY_WRITABLE_HOOKS, run the following command: grep CONFIG_SECURITY_WRITABLE_HOOKS /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R20 If CONFIG_SECURITY_WRITABLE_HOOKS is enabled, then hooks can be loaded at runtime and being able to manipulate hooks is a way to bypass all LSMs. Enable Yama support This enables support for LSM module Yama, which extends DAC support with additional system-wide security settings beyond regular Linux discretionary access controls. The module will limit the use of the system call ptrace(). The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SECURITY_YAMA, run the following command: grep CONFIG_SECURITY_YAMA /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R20 Unrestricted usage of ptrace allows compromised binaries to run ptrace on another processes of the user. Harden slab freelist metadata This feature protects integrity of the allocator's metadata. This configuration is available from kernel 4.14. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SLAB_FREELIST_HARDENED, run the following command: grep CONFIG_SLAB_FREELIST_HARDENED /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 Many kernel heap attacks try to target slab cache metadata and other infrastructure. This options makes minor performance sacrifices to harden the kernel slab allocator against common freelist exploit methods. Randomize slab freelist Randomizes the freelist order used on creating new pages. This configuration is available from kernel 5.9, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SLAB_FREELIST_RANDOM, run the following command: grep CONFIG_SLAB_FREELIST_RANDOM /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 This security feature reduces the predictability of the kernel slab allocator against heap overflows. Disallow merge of slab caches For reduced kernel memory fragmentation, slab caches can be merged when they share the same size and other characteristics. This carries a risk of kernel heap overflows being able to overwrite objects from merged caches (and more easily control cache layout), which makes such heap attacks easier to exploit by attackers. This configuration is available from kernel 4.13. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SLAB_MERGE_DEFAULT, run the following command: grep CONFIG_SLAB_MERGE_DEFAULT /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 Disabling the merge of slabs of similar sizes prevents the kernel from merging a seemingly useless but vulnerable slab with a useful and valuable slab. This increase the risk that a heap overflow could overwrite objects from merged caches, with unmerged caches the heap overflow would only affect the objects in the same cache. Overall, this reduces the kernel attack surface area by isolating slabs from each other. Enable SLUB debugging support SLUB has extensive debug support features and this allows the allocator validation checking to be enabled. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SLUB_DEBUG, run the following command: grep CONFIG_SLUB_DEBUG /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R17 This activates the checking of the memory allocator structures and resets to zero the zones allocated when they are released. Stack Protector buffer overlow detection This feature puts, at the beginning of functions, a canary value on the stack just before the return address, and validates the value just before actually returning. This configuration is available from kernel 4.18. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_STACKPROTECTOR, run the following command: grep CONFIG_STACKPROTECTOR /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This halts the program when a stack overflow is detected, potentially reducing the impact of exploits. Strong Stack Protector This features adds canary logic protection to more kinds of vulnerable functions than CONFIG_STACKPROTECTOR, but not to all functions so that performance is not severily impacted. This configuration is available from kernel 4.18. This config requires gcc version 4.9 or above, or a distribution gcc with the feature backported ("-fstack-protector-strong"). The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_STACKPROTECTOR_STRONG, run the following command: grep CONFIG_STACKPROTECTOR_STRONG /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This provides a mechanism that protects more vulnerable functions than CONFIG_STACKPROTECTOR, balancing between security and performance. Make the kernel text and rodata read-only When set, kernel text and rodata memory will be made read-only, and non-text memory will be made non-executable. This configuration is available from kernel 4.11. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_STRICT_KERNEL_RWX, run the following command: grep CONFIG_STRICT_KERNEL_RWX /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This provides protection against certain security exploits (e.g. executing the heap or modifying text) Make the module text and rodata read-only When set, module text and rodata memory will be made read-only, and non-text memory will be made non-executable. This configuration is available from kernel 4.11. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_STRICT_MODULE_RWX, run the following command: grep CONFIG_STRICT_MODULE_RWX /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R18 This provides protection against certain security exploits (e.g. executing the heap or modifying text) Enable TCP/IP syncookie support Normal TCP/IP networking is open to an attack known as SYN flooding. It is denial-of-service attack that prevents legitimate remote users from being able to connect to your computer during an ongoing attack. When enabled the TCP/IP stack will use a cryptographic challenge protocol known as SYN cookies to enable legitimate users to continue to connect, even when your machine is under attack. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_SYN_COOKIES, run the following command: grep CONFIG_SYN_COOKIES /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R22 SYN cookies provide protection against SYN flooding attacks. Unmap kernel when running in userspace (aka KAISER) Speculation attacks against some high-performance processors can be used to bypass MMU permission checks and leak kernel data to userspace. This can be defended against by unmapping the kernel when running in userspace, mapping it back in on exception entry via a trampoline page in the vector table. This configuration is available from kernel 4.16, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_UNMAP_KERNEL_AT_EL0, run the following command: grep CONFIG_UNMAP_KERNEL_AT_EL0 /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R27 This is a countermeasure to the Meltdown attack. User a virtually-mapped stack Enable this to use virtually-mapped kernel stacks with guard pages. This configuration is available from kernel 4.9. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_VMAP_STACK, run the following command: grep CONFIG_VMAP_STACK /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 This causes kernel stack overflows to be caught immediately rather than causing difficult-to-diagnose corruption. Disable x86 vsyscall emulation Disabling it is roughly equivalent to booting with vsyscall=none, except that it will also disable the helpful warning if a program tries to use a vsyscall. With this option set to N, offending programs will just segfault, citing addresses of the form 0xffffffffff600?00. This configuration is available from kernel 3.19. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_X86_VSYSCALL_EMULATION, run the following command: grep CONFIG_X86_VSYSCALL_EMULATION /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R15 The vsyscall table is no longer required and is a potential source of ROP gadgets. Kernel GCC plugin configuration Contains rules that check the configuration of GCC plugins used by the compiler Generate some entropy during boot and runtime Instrument some kernel code to extract some entropy from both original and artificially created program state. This will help especially embedded systems where there is little 'natural' source of entropy normally. This configuration is available from kernel 4.9, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_LATENT_ENTROPY, run the following command: grep CONFIG_GCC_PLUGIN_LATENT_ENTROPY /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. Note that entropy extracted this way is not cryptographically secure! There is a performance cost during the boot process (about 0.5%) and fork and irq processing. R21 This helps generate entropy during startup and is particularly relevant for devices with inappropriate entropy sources. Randomize layout of sensitive kernel structures Randomize at compile-time the layouts of structures that are entirely function pointers (and have not been manually annotated with __no_randomize_layout), or structures that have been explicitly marked with __randomize_layout. This configuration is available from kernel 4.13, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_RANDSTRUCT, run the following command: grep CONFIG_GCC_PLUGIN_RANDSTRUCT /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R21 Randomizing the layout of kernel data structures make it more difficult for an attacker to know the location of sensitive data. Poison kernel stack before returning from syscalls This option makes the kernel erase the kernel stack before returning from system calls. This has the effect of leaving the stack initialized to the poison value, which both reduces the lifetime of any sensitive stack contents and reduces potential for uninitialized stack variable exploits or information exposures (it does not cover functions reaching the same stack depth as prior functions during the same syscall). This configuration is available from kernel 4.20, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_STACKLEAK, run the following command: grep CONFIG_GCC_PLUGIN_STACKLEAK /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. The performance impact on a single CPU system kernel is of 1% slowdown. R21 This blocks most uninitialized stack variable attacks, with the performance impact being driven by the depth of the stack usage, rather than the function calling complexity. Force initialization of variables containing userspace addresses While the kernel is built with warnings enabled for any missed stack variable initializations, this warning is silenced for anything passed by reference to another function, under the occasionally misguided assumption that the function will do the initialization. As this regularly leads to exploitable flaws, this plugin is available to identify and zero-initialize such variables, depending on the chosen level of coverage. This configuration is available from kernel 4.11, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_STRUCTLEAK, run the following command: grep CONFIG_GCC_PLUGIN_STRUCTLEAK /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R21 Initializing structures from userspace can prevent some classes of information exposure. zero-init everything passed by reference Zero-initialize any stack variables that may be passed by reference and had not already been explicitly initialized. This configuration is available from kernel 4.14, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL, run the following command: grep CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL /boot/config-* For each kernel installed, a line with value "y" should be returned. There is no remediation for this besides re-compiling the kernel with the appropriate value for the config. R21 This eliminates all classes of uninitialized stack variable exploits and information exposures. Configure Syslog The syslog service has been the default Unix logging mechanism for many years. It has a number of downsides, including inconsistent log format, lack of authentication for received messages, and lack of authentication, encryption, or reliable transport for messages sent over a network. However, due to its long history, syslog is a de facto standard which is supported by almost all Unix applications. In Fedora, rsyslog has replaced ksyslogd as the syslog daemon of choice, and it includes some additional security features such as reliable, connection-oriented (i.e. TCP) transmission of logs, the option to log to database formats, and the encryption of log data en route to a central logging server. This section discusses how to configure rsyslog for best effect, and how to use tools provided with the system to maintain and monitor logs. Ensure rsyslog-gnutls is installed TLS protocol support for rsyslog is installed. The rsyslog-gnutls package can be installed with the following command: $ sudo dnf install rsyslog-gnutls SRG-OS-000480-GPOS-00227 SRG-OS-000120-GPOS-00061 R71 The rsyslog-gnutls package provides Transport Layer Security (TLS) support for the rsyslog daemon, which enables secure remote logging. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "rsyslog-gnutls" ; then dnf install -y "rsyslog-gnutls" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_rsyslog-gnutls_installed - name: Ensure rsyslog-gnutls is installed ansible.builtin.package: name: rsyslog-gnutls state: present when: '"kernel" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_rsyslog-gnutls_installed include install_rsyslog-gnutls class install_rsyslog-gnutls { package { 'rsyslog-gnutls': ensure => 'installed', } } package --add=rsyslog-gnutls [[packages]] name = "rsyslog-gnutls" version = "*" package install rsyslog-gnutls dnf install rsyslog-gnutls Ensure rsyslog is Installed Rsyslog is installed by default. The rsyslog package can be installed with the following command: $ sudo dnf install rsyslog 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 164.312(a)(2)(ii) 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) PR.PT-1 SRG-OS-000479-GPOS-00224 SRG-OS-000051-GPOS-00024 SRG-OS-000480-GPOS-00227 The rsyslog package provides the rsyslog daemon, which provides system logging services. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "rsyslog" ; then dnf install -y "rsyslog" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_rsyslog_installed - name: Ensure rsyslog is installed ansible.builtin.package: name: rsyslog state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_rsyslog_installed include install_rsyslog class install_rsyslog { package { 'rsyslog': ensure => 'installed', } } package --add=rsyslog [[packages]] name = "rsyslog" version = "*" package install rsyslog dnf install rsyslog Enable rsyslog Service The rsyslog service provides syslog-style logging by default on Fedora. The rsyslog service can be enabled with the following command: $ sudo systemctl enable rsyslog.service 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO13.01 BAI03.05 BAI04.04 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 164.312(a)(2)(ii) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 A.17.2.1 CM-6(a) AU-4(1) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.DS-4 PR.PT-1 SRG-OS-000480-GPOS-00227 The rsyslog service must be running in order to provide logging services, which are essential to system administration. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'rsyslog.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'rsyslog.service' fi "$SYSTEMCTL_EXEC" enable 'rsyslog.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-4(1) - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_rsyslog_enabled - name: Enable rsyslog Service - Enable service rsyslog block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable rsyslog Service - Enable Service rsyslog ansible.builtin.systemd: name: rsyslog enabled: true state: started masked: false when: - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AU-4(1) - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_rsyslog_enabled - special_service_block when: '"kernel" in ansible_facts.packages' include enable_rsyslog class enable_rsyslog { service {'rsyslog': enable => true, ensure => 'running', } } [customizations.services] enabled = ["rsyslog"] service enable rsyslog Disable Logwatch on Clients if a Logserver Exists Does your site have a central logserver which has been configured to report on logs received from all systems? If so: $ sudo rm /etc/cron.daily/0logwatch If no logserver exists, it will be necessary for each system to run Logwatch individually. Using a central logserver provides the security and reliability benefits discussed earlier, and also makes monitoring logs easier and less time-intensive for administrators. Configure Logwatch on the Central Log Server Is this system the central log server? If so, edit the file /etc/logwatch/conf/logwatch.conf as shown below. Configure Logwatch HostLimit Line On a central logserver, you want Logwatch to summarize all syslog entries, including those which did not originate on the logserver itself. The HostLimit setting tells Logwatch to report on all hosts, not just the one on which it is running. HostLimit = no Configure Logwatch SplitHosts Line If SplitHosts is set, Logwatch will separate entries by hostname. This makes the report longer but significantly more usable. If it is not set, then Logwatch will not report which host generated a given log entry, and that information is almost always necessary SplitHosts = yes Ensure Proper Configuration of Log Files The file /etc/rsyslog.conf controls where log message are written. These are controlled by lines called rules, which consist of a selector and an action. These rules are often customized depending on the role of the system, the requirements of the environment, and whatever may enable the administrator to most effectively make use of log data. The default rules in Fedora are: *.info;mail.none;authpriv.none;cron.none /var/log/messages authpriv.* /var/log/secure mail.* -/var/log/maillog cron.* /var/log/cron *.emerg * uucp,news.crit /var/log/spooler local7.* /var/log/boot.log See the man page rsyslog.conf(5) for more information. Note that the rsyslog daemon can be configured to use a timestamp format that some log processing programs may not understand. If this occurs, edit the file /etc/rsyslog.conf and add or edit the following line: $ ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat Ensure cron Is Logging To Rsyslog Cron logging must be implemented to spot intrusions or trace cron job status. If cron is not logging to rsyslog, it can be implemented by adding the following to the RULES section of /etc/rsyslog.conf: If the legacy syntax is used: cron.* /var/log/cron If the modern syntax (RainerScript) is used: cron.* action(type="omfile" file="/var/log/cron") 1 14 15 16 3 5 6 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 0988 1405 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.15.2.1 A.15.2.2 CM-6(a) ID.SC-4 PR.PT-1 SRG-OS-000480-GPOS-00227 Cron logging can be used to trace the successful or unsuccessful execution of cron jobs. It can also be used to spot intrusions into the use of the cron facility by unauthorized and malicious users. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then if ! grep -Pzo '(?m)^\s*cron\.\*\s*(/var/log/cron|action\(\s*.*(?i:\btype\b)="omfile"\s*.*(?i:\bfile\b)="/var/log/cron"\s*\))\s*$' /etc/rsyslog.conf /etc/rsyslog.d/*.conf; then mkdir -p /etc/rsyslog.d echo "cron.* /var/log/cron" >> /etc/rsyslog.d/cron.conf fi if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then systemctl restart rsyslog.service fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_cron_logging - name: Ensure cron Is Logging To Rsyslog - Search if cron configuration exists ansible.builtin.command: grep -Pzo '(?m)^\s*cron\.\*\s*(/var/log/cron|action\(\s*.*(?i:\btype\b)="omfile"\s*.*(?i:\bfile\b)="/var/log/cron"\s*\))\s*$' /etc/rsyslog.conf /etc/rsyslog.d/*.conf register: cron_log_config_exists failed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_cron_logging - name: Ensure cron Is Logging To Rsyslog - Ensure the /etc/rsyslog.d directory exists ansible.builtin.file: path: /etc/rsyslog.d state: directory when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_cron_logging - name: Ensure cron Is Logging To Rsyslog - Add cron log configuration line ansible.builtin.lineinfile: path: /etc/rsyslog.d/cron.conf line: cron.* /var/log/cron create: true when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - cron_log_config_exists.stdout_lines | length == 0 tags: - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_cron_logging - name: Ensure cron Is Logging To Rsyslog - Restart the rsyslog service now ansible.builtin.service: name: rsyslog state: restarted when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_cron_logging Ensure Rsyslog Authenticates Off-Loaded Audit Records Rsyslogd is a system utility providing support for message logging. Support for both internet and UNIX domain sockets enables this utility to support both local and remote logging. Couple this utility with gnutls (which is a secure communications library implementing the SSL, TLS and DTLS protocols), and you have a method to securely encrypt and off-load auditing. When using rsyslogd to off-load logs the remote system must be authenticated. Set the following configuration option in /etc/rsyslog.conf or in a file in /etc/rsyslog.d (using legacy syntax): $ActionSendStreamDriverAuthMode x509/name Alternatively, use the RainerScript syntax: action(type="omfwd" Target="some.example.com" StreamDriverAuthMode="x509/name") AU-4(1) SRG-OS-000342-GPOS-00133 SRG-OS-000479-GPOS-00224 The audit records generated by Rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Audit records should be protected from unauthorized access. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then sed -i '/^.*\$ActionSendStreamDriverAuthMode.*/d' /etc/rsyslog.conf /etc/rsyslog.d/*.conf 2> /dev/null if [ -e "/etc/rsyslog.d/stream_driver_auth.conf" ] ; then LC_ALL=C sed -i "/^\s*\$ActionSendStreamDriverAuthMode /Id" "/etc/rsyslog.d/stream_driver_auth.conf" else touch "/etc/rsyslog.d/stream_driver_auth.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/rsyslog.d/stream_driver_auth.conf" cp "/etc/rsyslog.d/stream_driver_auth.conf" "/etc/rsyslog.d/stream_driver_auth.conf.bak" # Insert at the end of the file printf '%s\n' "\$ActionSendStreamDriverAuthMode x509/name" >> "/etc/rsyslog.d/stream_driver_auth.conf" # Clean up after ourselves. rm "/etc/rsyslog.d/stream_driver_auth.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-4(1) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_encrypt_offload_actionsendstreamdriverauthmode - name: Ensure Rsyslog Authenticates Off-Loaded Audit Records block: - name: Deduplicate values from /etc/rsyslog.conf ansible.builtin.lineinfile: path: /etc/rsyslog.conf create: false regexp: (?i)^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s state: absent - name: Check if /etc/rsyslog.d exists ansible.builtin.stat: path: /etc/rsyslog.d register: _etc_rsyslog_d_exists - name: Check if the parameter $ActionSendStreamDriverAuthMode is present in /etc/rsyslog.d ansible.builtin.find: paths: /etc/rsyslog.d recurse: 'yes' follow: 'no' contains: ^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s register: _etc_rsyslog_d_has_parameter when: _etc_rsyslog_d_exists.stat.isdir is defined and _etc_rsyslog_d_exists.stat.isdir - name: Remove parameter from files in /etc/rsyslog.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s state: absent with_items: '{{ _etc_rsyslog_d_has_parameter.files }}' when: _etc_rsyslog_d_has_parameter.matched - name: Insert correct line to /etc/rsyslog.conf ansible.builtin.lineinfile: path: /etc/rsyslog.conf create: true regexp: (?i)^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s line: $ActionSendStreamDriverAuthMode x509/name state: present when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AU-4(1) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_encrypt_offload_actionsendstreamdriverauthmode Ensure Rsyslog Encrypts Off-Loaded Audit Records Rsyslogd is a system utility providing support for message logging. Support for both internet and UNIX domain sockets enables this utility to support both local and remote logging. Couple this utility with gnutls (which is a secure communications library implementing the SSL, TLS and DTLS protocols), and you have a method to securely encrypt and off-load auditing. When using rsyslogd to off-load logs off a encrpytion system must be used. Set the following configuration option in /etc/rsyslog.conf or in a file in /etc/rsyslog.d (using legacy syntax): $ActionSendStreamDriverMode 1 Alternatively, use the RainerScript syntax: action(type="omfwd" ... StreamDriverMode="1") AU-4(1) SRG-OS-000342-GPOS-00133 SRG-OS-000479-GPOS-00224 The audit records generated by Rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Audit records should be protected from unauthorized access. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then if [ -e "/etc/rsyslog.d/encrypt.conf" ] ; then LC_ALL=C sed -i "/^\s*\$ActionSendStreamDriverMode /Id" "/etc/rsyslog.d/encrypt.conf" else touch "/etc/rsyslog.d/encrypt.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/rsyslog.d/encrypt.conf" cp "/etc/rsyslog.d/encrypt.conf" "/etc/rsyslog.d/encrypt.conf.bak" # Insert at the end of the file printf '%s\n' "\$ActionSendStreamDriverMode 1" >> "/etc/rsyslog.d/encrypt.conf" # Clean up after ourselves. rm "/etc/rsyslog.d/encrypt.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-4(1) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_encrypt_offload_actionsendstreamdrivermode - name: Ensure Rsyslog Encrypts Off-Loaded Audit Records block: - name: Deduplicate values from /etc/rsyslog.conf ansible.builtin.lineinfile: path: /etc/rsyslog.conf create: false regexp: '(?i)^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} ' state: absent - name: Check if /etc/rsyslog.d exists ansible.builtin.stat: path: /etc/rsyslog.d register: _etc_rsyslog_d_exists - name: Check if the parameter $ActionSendStreamDriverMode is present in /etc/rsyslog.d ansible.builtin.find: paths: /etc/rsyslog.d recurse: 'yes' follow: 'no' contains: '^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} ' register: _etc_rsyslog_d_has_parameter when: _etc_rsyslog_d_exists.stat.isdir is defined and _etc_rsyslog_d_exists.stat.isdir - name: Remove parameter from files in /etc/rsyslog.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: '(?i)^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} ' state: absent with_items: '{{ _etc_rsyslog_d_has_parameter.files }}' when: _etc_rsyslog_d_has_parameter.matched - name: Insert correct line to /etc/rsyslog.conf ansible.builtin.lineinfile: path: /etc/rsyslog.conf create: true regexp: '(?i)^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} ' line: $ActionSendStreamDriverMode 1 state: present when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AU-4(1) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_encrypt_offload_actionsendstreamdrivermode Ensure Rsyslog Encrypts Off-Loaded Audit Records Rsyslogd is a system utility providing support for message logging. Support for both internet and UNIX domain sockets enables this utility to support both local and remote logging. Couple this utility with gnutls (which is a secure communications library implementing the SSL, TLS and DTLS protocols), and you have a method to securely encrypt and off-load auditing. When using rsyslogd to off-load logs off an encryption system must be used. Set the following configuration option in /etc/rsyslog.conf or in a file in /etc/rsyslog.d (using legacy syntax): $DefaultNetstreamDriver gtls Alternatively, use the RainerScript syntax: global(DefaultNetstreamDriver="gtls") AU-4(1) SRG-OS-000342-GPOS-00133 SRG-OS-000479-GPOS-00224 The audit records generated by Rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Audit records should be protected from unauthorized access. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then if [ -e "/etc/rsyslog.d/encrypt.conf" ] ; then LC_ALL=C sed -i "/^\s*\$DefaultNetstreamDriver /Id" "/etc/rsyslog.d/encrypt.conf" else touch "/etc/rsyslog.d/encrypt.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/rsyslog.d/encrypt.conf" cp "/etc/rsyslog.d/encrypt.conf" "/etc/rsyslog.d/encrypt.conf.bak" # Insert at the end of the file printf '%s\n' "\$DefaultNetstreamDriver gtls" >> "/etc/rsyslog.d/encrypt.conf" # Clean up after ourselves. rm "/etc/rsyslog.d/encrypt.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-4(1) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_encrypt_offload_defaultnetstreamdriver - name: Ensure Rsyslog Encrypts Off-Loaded Audit Records block: - name: Deduplicate values from /etc/rsyslog.conf ansible.builtin.lineinfile: path: /etc/rsyslog.conf create: false regexp: '(?i)^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} ' state: absent - name: Check if /etc/rsyslog.d exists ansible.builtin.stat: path: /etc/rsyslog.d register: _etc_rsyslog_d_exists - name: Check if the parameter $DefaultNetstreamDriver is present in /etc/rsyslog.d ansible.builtin.find: paths: /etc/rsyslog.d recurse: 'yes' follow: 'no' contains: '^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} ' register: _etc_rsyslog_d_has_parameter when: _etc_rsyslog_d_exists.stat.isdir is defined and _etc_rsyslog_d_exists.stat.isdir - name: Remove parameter from files in /etc/rsyslog.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: '(?i)^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} ' state: absent with_items: '{{ _etc_rsyslog_d_has_parameter.files }}' when: _etc_rsyslog_d_has_parameter.matched - name: Insert correct line to /etc/rsyslog.conf ansible.builtin.lineinfile: path: /etc/rsyslog.conf create: true regexp: '(?i)^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} ' line: $DefaultNetstreamDriver gtls state: present when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AU-4(1) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_encrypt_offload_defaultnetstreamdriver Ensure Log Files Are Owned By Appropriate Group The group-owner of all log files written by rsyslog should be root. These log files are determined by the second part of each Rule line in /etc/rsyslog.conf and typically all appear in /var/log. For each log file LOGFILE referenced in /etc/rsyslog.conf, run the following command to inspect the file's group owner: $ ls -l LOGFILE If the owner is not root, run the following command to correct this: $ sudo chgrp root LOGFILE 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 0988 1405 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-10.5.1 Req-10.5.2 R71 6.2.6.1 10.3.2 10.3 The log files generated by rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Log files should be protected from unauthorized access. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then # List of log file paths to be inspected for correct permissions # * Primarily inspect log file paths listed in /etc/rsyslog.conf RSYSLOG_ETC_CONFIG="/etc/rsyslog.conf" # * And also the log file paths listed after rsyslog's $IncludeConfig directive # (store the result into array for the case there's shell glob used as value of IncludeConfig) readarray -t OLD_INC < <(grep -e "\$IncludeConfig[[:space:]]\+[^[:space:];]\+" /etc/rsyslog.conf | cut -d ' ' -f 2) readarray -t RSYSLOG_INCLUDE_CONFIG < <(for INCPATH in "${OLD_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done) readarray -t NEW_INC < <(sed -n '/^\s*include(/,/)/Ip' /etc/rsyslog.conf | sed -n 's@.*file\s*=\s*"\([/[:alnum:][:punct:]]*\)".*@\1@Ip') readarray -t RSYSLOG_INCLUDE < <(for INCPATH in "${NEW_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done) # Declare an array to hold the final list of different log file paths declare -a LOG_FILE_PATHS # Array to hold all rsyslog config entries RSYSLOG_CONFIGS=() RSYSLOG_CONFIGS=("${RSYSLOG_ETC_CONFIG}" "${RSYSLOG_INCLUDE_CONFIG[@]}" "${RSYSLOG_INCLUDE[@]}") # Get full list of files to be checked # RSYSLOG_CONFIGS may contain globs such as # /etc/rsyslog.d/*.conf /etc/rsyslog.d/*.frule # So, loop over the entries in RSYSLOG_CONFIGS and use find to get the list of included files. RSYSLOG_CONFIG_FILES=() for ENTRY in "${RSYSLOG_CONFIGS[@]}" do # If directory, rsyslog will search for config files in recursively. # However, files in hidden sub-directories or hidden files will be ignored. if [ -d "${ENTRY}" ] then readarray -t FINDOUT < <(find "${ENTRY}" -not -path '*/.*' -type f) RSYSLOG_CONFIG_FILES+=("${FINDOUT[@]}") elif [ -f "${ENTRY}" ] then RSYSLOG_CONFIG_FILES+=("${ENTRY}") else echo "Invalid include object: ${ENTRY}" fi done # Browse each file selected above as containing paths of log files # ('/etc/rsyslog.conf' and '/etc/rsyslog.d/*.conf' in the default configuration) for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}" do # From each of these files extract just particular log file path(s), thus: # * Ignore lines starting with space (' '), comment ('#"), or variable syntax ('$') characters, # * Ignore empty lines, # * Strip quotes and closing brackets from paths. # * Ignore paths that match /dev|/etc.*\.conf, as those are paths, but likely not log files # * From the remaining valid rows select only fields constituting a log file path # Text file column is understood to represent a log file path if and only if all of the # following are met: # * it contains at least one slash '/' character, # * it is preceded by space # * it doesn't contain space (' '), colon (':'), and semicolon (';') characters # Search log file for path(s) only in case it exists! if [[ -f "${LOG_FILE}" ]] then NORMALIZED_CONFIG_FILE_LINES=$(sed -e "/^[#|$]/d" "${LOG_FILE}") LINES_WITH_PATHS=$(grep '[^/]*\s\+\S*/\S\+$' <<< "${NORMALIZED_CONFIG_FILE_LINES}") FILTERED_PATHS=$(awk '{if(NF>=2&&($NF~/^\//||$NF~/^-\//)){sub(/^-\//,"/",$NF);print $NF}}' <<< "${LINES_WITH_PATHS}") CLEANED_PATHS=$(sed -e "s/[\"')]//g; /\\/etc.*\.conf/d; /\\/dev\\//d" <<< "${FILTERED_PATHS}") MATCHED_ITEMS=$(sed -e "/^$/d" <<< "${CLEANED_PATHS}") # Since above sed command might return more than one item (delimited by newline), split # the particular matches entries into new array specific for this log file readarray -t ARRAY_FOR_LOG_FILE <<< "$MATCHED_ITEMS" # Concatenate the two arrays - previous content of $LOG_FILE_PATHS array with # items from newly created array for this log file LOG_FILE_PATHS+=("${ARRAY_FOR_LOG_FILE[@]}") # Delete the temporary array unset ARRAY_FOR_LOG_FILE fi done # Check for RainerScript action log format which might be also multiline so grep regex is a bit # curly: # extract possibly multiline action omfile expressions # extract File="logfile" expression # match only "logfile" expression for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}" do ACTION_OMFILE_LINES=$(grep -iozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" "${LOG_FILE}") OMFILE_LINES=$(echo "${ACTION_OMFILE_LINES}"| grep -iaoP "\bFile\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)") LOG_FILE_PATHS+=("$(echo "${OMFILE_LINES}"| grep -oE "\"([/[:alnum:][:punct:]]*)\""|tr -d "\"")") done # Ensure the correct attribute if file exists FILE_CMD="chgrp" for LOG_FILE_PATH in "${LOG_FILE_PATHS[@]}" do # Sanity check - if particular $LOG_FILE_PATH is empty string, skip it from further processing if [ -z "$LOG_FILE_PATH" ] then continue fi $FILE_CMD "root" "$LOG_FILE_PATH" done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - Set rsyslog logfile configuration facts ansible.builtin.set_fact: rsyslog_etc_config: /etc/rsyslog.conf when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - Get IncludeConfig directive ansible.builtin.shell: | set -o pipefail grep -e '$IncludeConfig' {{ rsyslog_etc_config }} | cut -d ' ' -f 2 || true register: rsyslog_old_inc changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - Get include files directives ansible.builtin.shell: | set -o pipefail awk '/)/{f=0} /include\(/{f=1} f{ nf=gensub("^(include\\(|\\s*)file=\"(\\S+)\".*","\\2",1); if($0!=nf){ print nf }}' {{ rsyslog_etc_config }} || true register: rsyslog_new_inc changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - Aggregate rsyslog includes ansible.builtin.set_fact: include_config_output: '{{ rsyslog_old_inc.stdout_lines + rsyslog_new_inc.stdout_lines }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_old_inc is not skipped and rsyslog_new_inc is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - List all config files ansible.builtin.find: paths: '{{ item | dirname }}' patterns: '{{ item | basename }}' hidden: false follow: true loop: '{{ include_config_output | list + [rsyslog_etc_config] }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - include_config_output is defined register: rsyslog_config_files failed_when: false changed_when: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - Extract log files old format ansible.builtin.shell: | set -o pipefail grep -oP '^[^(\s|#|\$)]+[\s]+.*[\s]+-?(/+[^:;\s]+);*\.*$' {{ item.1.path }} | \ awk '{print $NF}' | \ sed -e 's/^-//' || true loop: '{{ rsyslog_config_files.results | default([]) | subelements(''files'') }}' register: log_files_old changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_config_files is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - Extract log files new format ansible.builtin.shell: | set -o pipefail grep -ozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" {{ item.1.path }} | \ grep -aoP "\bFile\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)" | \ grep -oE "\"([/[:alnum:][:punct:]]*)\"" | \ tr -d "\""|| true loop: '{{ rsyslog_config_files.results | default([]) | subelements(''files'') }}' register: log_files_new changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_config_files is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group - Sum all log files found ansible.builtin.set_fact: log_files: '{{ log_files_new.results | map(attribute=''stdout_lines'') | list | flatten | unique + log_files_old.results | map(attribute=''stdout_lines'') | list | flatten | unique }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership - name: Ensure Log Files Are Owned By Appropriate Group -Setup log files attribute ansible.builtin.file: path: '{{ item }}' group: root state: file loop: '{{ log_files | list | flatten | unique }}' failed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_groupownership Ensure Log Files Are Owned By Appropriate User The owner of all log files written by rsyslog should be root. These log files are determined by the second part of each Rule line in /etc/rsyslog.conf and typically all appear in /var/log. For each log file LOGFILE referenced in /etc/rsyslog.conf, run the following command to inspect the file's owner: $ ls -l LOGFILE If the owner is not root, run the following command to correct this: $ sudo chown root LOGFILE 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 0988 1405 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-10.5.1 Req-10.5.2 R71 6.2.6.1 10.3.2 10.3 The log files generated by rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Log files should be protected from unauthorized access. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then # List of log file paths to be inspected for correct permissions # * Primarily inspect log file paths listed in /etc/rsyslog.conf RSYSLOG_ETC_CONFIG="/etc/rsyslog.conf" # * And also the log file paths listed after rsyslog's $IncludeConfig directive # (store the result into array for the case there's shell glob used as value of IncludeConfig) readarray -t OLD_INC < <(grep -e "\$IncludeConfig[[:space:]]\+[^[:space:];]\+" /etc/rsyslog.conf | cut -d ' ' -f 2) readarray -t RSYSLOG_INCLUDE_CONFIG < <(for INCPATH in "${OLD_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done) readarray -t NEW_INC < <(sed -n '/^\s*include(/,/)/Ip' /etc/rsyslog.conf | sed -n 's@.*file\s*=\s*"\([/[:alnum:][:punct:]]*\)".*@\1@Ip') readarray -t RSYSLOG_INCLUDE < <(for INCPATH in "${NEW_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done) # Declare an array to hold the final list of different log file paths declare -a LOG_FILE_PATHS # Array to hold all rsyslog config entries RSYSLOG_CONFIGS=() RSYSLOG_CONFIGS=("${RSYSLOG_ETC_CONFIG}" "${RSYSLOG_INCLUDE_CONFIG[@]}" "${RSYSLOG_INCLUDE[@]}") # Get full list of files to be checked # RSYSLOG_CONFIGS may contain globs such as # /etc/rsyslog.d/*.conf /etc/rsyslog.d/*.frule # So, loop over the entries in RSYSLOG_CONFIGS and use find to get the list of included files. RSYSLOG_CONFIG_FILES=() for ENTRY in "${RSYSLOG_CONFIGS[@]}" do # If directory, rsyslog will search for config files in recursively. # However, files in hidden sub-directories or hidden files will be ignored. if [ -d "${ENTRY}" ] then readarray -t FINDOUT < <(find "${ENTRY}" -not -path '*/.*' -type f) RSYSLOG_CONFIG_FILES+=("${FINDOUT[@]}") elif [ -f "${ENTRY}" ] then RSYSLOG_CONFIG_FILES+=("${ENTRY}") else echo "Invalid include object: ${ENTRY}" fi done # Browse each file selected above as containing paths of log files # ('/etc/rsyslog.conf' and '/etc/rsyslog.d/*.conf' in the default configuration) for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}" do # From each of these files extract just particular log file path(s), thus: # * Ignore lines starting with space (' '), comment ('#"), or variable syntax ('$') characters, # * Ignore empty lines, # * Strip quotes and closing brackets from paths. # * Ignore paths that match /dev|/etc.*\.conf, as those are paths, but likely not log files # * From the remaining valid rows select only fields constituting a log file path # Text file column is understood to represent a log file path if and only if all of the # following are met: # * it contains at least one slash '/' character, # * it is preceded by space # * it doesn't contain space (' '), colon (':'), and semicolon (';') characters # Search log file for path(s) only in case it exists! if [[ -f "${LOG_FILE}" ]] then NORMALIZED_CONFIG_FILE_LINES=$(sed -e "/^[#|$]/d" "${LOG_FILE}") LINES_WITH_PATHS=$(grep '[^/]*\s\+\S*/\S\+$' <<< "${NORMALIZED_CONFIG_FILE_LINES}") FILTERED_PATHS=$(awk '{if(NF>=2&&($NF~/^\//||$NF~/^-\//)){sub(/^-\//,"/",$NF);print $NF}}' <<< "${LINES_WITH_PATHS}") CLEANED_PATHS=$(sed -e "s/[\"')]//g; /\\/etc.*\.conf/d; /\\/dev\\//d" <<< "${FILTERED_PATHS}") MATCHED_ITEMS=$(sed -e "/^$/d" <<< "${CLEANED_PATHS}") # Since above sed command might return more than one item (delimited by newline), split # the particular matches entries into new array specific for this log file readarray -t ARRAY_FOR_LOG_FILE <<< "$MATCHED_ITEMS" # Concatenate the two arrays - previous content of $LOG_FILE_PATHS array with # items from newly created array for this log file LOG_FILE_PATHS+=("${ARRAY_FOR_LOG_FILE[@]}") # Delete the temporary array unset ARRAY_FOR_LOG_FILE fi done # Check for RainerScript action log format which might be also multiline so grep regex is a bit # curly: # extract possibly multiline action omfile expressions # extract File="logfile" expression # match only "logfile" expression for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}" do ACTION_OMFILE_LINES=$(grep -iozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" "${LOG_FILE}") OMFILE_LINES=$(echo "${ACTION_OMFILE_LINES}"| grep -iaoP "\bFile\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)") LOG_FILE_PATHS+=("$(echo "${OMFILE_LINES}"| grep -oE "\"([/[:alnum:][:punct:]]*)\""|tr -d "\"")") done # Ensure the correct attribute if file exists FILE_CMD="chown" for LOG_FILE_PATH in "${LOG_FILE_PATHS[@]}" do # Sanity check - if particular $LOG_FILE_PATH is empty string, skip it from further processing if [ -z "$LOG_FILE_PATH" ] then continue fi $FILE_CMD "root" "$LOG_FILE_PATH" done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - Set rsyslog logfile configuration facts ansible.builtin.set_fact: rsyslog_etc_config: /etc/rsyslog.conf when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - Get IncludeConfig directive ansible.builtin.shell: | set -o pipefail grep -e '$IncludeConfig' {{ rsyslog_etc_config }} | cut -d ' ' -f 2 || true register: rsyslog_old_inc changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - Get include files directives ansible.builtin.shell: | set -o pipefail awk '/)/{f=0} /include\(/{f=1} f{ nf=gensub("^(include\\(|\\s*)file=\"(\\S+)\".*","\\2",1); if($0!=nf){ print nf }}' {{ rsyslog_etc_config }} || true register: rsyslog_new_inc changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - Aggregate rsyslog includes ansible.builtin.set_fact: include_config_output: '{{ rsyslog_old_inc.stdout_lines + rsyslog_new_inc.stdout_lines }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_old_inc is not skipped and rsyslog_new_inc is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - List all config files ansible.builtin.find: paths: '{{ item | dirname }}' patterns: '{{ item | basename }}' hidden: false follow: true loop: '{{ include_config_output | list + [rsyslog_etc_config] }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - include_config_output is defined register: rsyslog_config_files failed_when: false changed_when: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - Extract log files old format ansible.builtin.shell: | set -o pipefail grep -oP '^[^(\s|#|\$)]+[\s]+.*[\s]+-?(/+[^:;\s]+);*\.*$' {{ item.1.path }} | \ awk '{print $NF}' | \ sed -e 's/^-//' || true loop: '{{ rsyslog_config_files.results | default([]) | subelements(''files'') }}' register: log_files_old changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_config_files is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - Extract log files new format ansible.builtin.shell: | set -o pipefail grep -ozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" {{ item.1.path }} | \ grep -aoP "\bFile\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)" | \ grep -oE "\"([/[:alnum:][:punct:]]*)\"" | \ tr -d "\""|| true loop: '{{ rsyslog_config_files.results | default([]) | subelements(''files'') }}' register: log_files_new changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_config_files is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User - Sum all log files found ansible.builtin.set_fact: log_files: '{{ log_files_new.results | map(attribute=''stdout_lines'') | list | flatten | unique + log_files_old.results | map(attribute=''stdout_lines'') | list | flatten | unique }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership - name: Ensure Log Files Are Owned By Appropriate User -Setup log files attribute ansible.builtin.file: path: '{{ item }}' owner: root state: file loop: '{{ log_files | list | flatten | unique }}' failed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_ownership Ensure System Log Files Have Correct Permissions The file permissions for all log files written by rsyslog should be set to 640, or more restrictive. These log files are determined by the second part of each Rule line in /etc/rsyslog.conf and typically all appear in /var/log. For each log file LOGFILE referenced in /etc/rsyslog.conf, run the following command to inspect the file's permissions: $ ls -l LOGFILE If the permissions are not 640 or more restrictive, run the following command to correct this: $ sudo chmod 640 LOGFILE " 0988 1405 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) Req-10.5.1 Req-10.5.2 R71 6.2.6.1 10.3.1 10.3 Log files can contain valuable information regarding system configuration. If the system log files are not protected unauthorized users could change the logged data, eliminating their forensic value. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then # List of log file paths to be inspected for correct permissions # * Primarily inspect log file paths listed in /etc/rsyslog.conf RSYSLOG_ETC_CONFIG="/etc/rsyslog.conf" # * And also the log file paths listed after rsyslog's $IncludeConfig directive # (store the result into array for the case there's shell glob used as value of IncludeConfig) readarray -t OLD_INC < <(grep -e "\$IncludeConfig[[:space:]]\+[^[:space:];]\+" /etc/rsyslog.conf | cut -d ' ' -f 2) readarray -t RSYSLOG_INCLUDE_CONFIG < <(for INCPATH in "${OLD_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done) readarray -t NEW_INC < <(sed -n '/^\s*include(/,/)/Ip' /etc/rsyslog.conf | sed -n 's@.*file\s*=\s*"\([/[:alnum:][:punct:]]*\)".*@\1@Ip') readarray -t RSYSLOG_INCLUDE < <(for INCPATH in "${NEW_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done) # Declare an array to hold the final list of different log file paths declare -a LOG_FILE_PATHS # Array to hold all rsyslog config entries RSYSLOG_CONFIGS=() RSYSLOG_CONFIGS=("${RSYSLOG_ETC_CONFIG}" "${RSYSLOG_INCLUDE_CONFIG[@]}" "${RSYSLOG_INCLUDE[@]}") # Get full list of files to be checked # RSYSLOG_CONFIGS may contain globs such as # /etc/rsyslog.d/*.conf /etc/rsyslog.d/*.frule # So, loop over the entries in RSYSLOG_CONFIGS and use find to get the list of included files. RSYSLOG_CONFIG_FILES=() for ENTRY in "${RSYSLOG_CONFIGS[@]}" do # If directory, rsyslog will search for config files in recursively. # However, files in hidden sub-directories or hidden files will be ignored. if [ -d "${ENTRY}" ] then readarray -t FINDOUT < <(find "${ENTRY}" -not -path '*/.*' -type f) RSYSLOG_CONFIG_FILES+=("${FINDOUT[@]}") elif [ -f "${ENTRY}" ] then RSYSLOG_CONFIG_FILES+=("${ENTRY}") else echo "Invalid include object: ${ENTRY}" fi done # Browse each file selected above as containing paths of log files # ('/etc/rsyslog.conf' and '/etc/rsyslog.d/*.conf' in the default configuration) for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}" do # From each of these files extract just particular log file path(s), thus: # * Ignore lines starting with space (' '), comment ('#"), or variable syntax ('$') characters, # * Ignore empty lines, # * Strip quotes and closing brackets from paths. # * Ignore paths that match /dev|/etc.*\.conf, as those are paths, but likely not log files # * From the remaining valid rows select only fields constituting a log file path # Text file column is understood to represent a log file path if and only if all of the # following are met: # * it contains at least one slash '/' character, # * it is preceded by space # * it doesn't contain space (' '), colon (':'), and semicolon (';') characters # Search log file for path(s) only in case it exists! if [[ -f "${LOG_FILE}" ]] then NORMALIZED_CONFIG_FILE_LINES=$(sed -e "/^[#|$]/d" "${LOG_FILE}") LINES_WITH_PATHS=$(grep '[^/]*\s\+\S*/\S\+$' <<< "${NORMALIZED_CONFIG_FILE_LINES}") FILTERED_PATHS=$(awk '{if(NF>=2&&($NF~/^\//||$NF~/^-\//)){sub(/^-\//,"/",$NF);print $NF}}' <<< "${LINES_WITH_PATHS}") CLEANED_PATHS=$(sed -e "s/[\"')]//g; /\\/etc.*\.conf/d; /\\/dev\\//d" <<< "${FILTERED_PATHS}") MATCHED_ITEMS=$(sed -e "/^$/d" <<< "${CLEANED_PATHS}") # Since above sed command might return more than one item (delimited by newline), split # the particular matches entries into new array specific for this log file readarray -t ARRAY_FOR_LOG_FILE <<< "$MATCHED_ITEMS" # Concatenate the two arrays - previous content of $LOG_FILE_PATHS array with # items from newly created array for this log file LOG_FILE_PATHS+=("${ARRAY_FOR_LOG_FILE[@]}") # Delete the temporary array unset ARRAY_FOR_LOG_FILE fi done # Check for RainerScript action log format which might be also multiline so grep regex is a bit # curly: # extract possibly multiline action omfile expressions # extract File="logfile" expression # match only "logfile" expression for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}" do ACTION_OMFILE_LINES=$(grep -iozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" "${LOG_FILE}") OMFILE_LINES=$(echo "${ACTION_OMFILE_LINES}"| grep -iaoP "\bFile\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)") LOG_FILE_PATHS+=("$(echo "${OMFILE_LINES}"| grep -oE "\"([/[:alnum:][:punct:]]*)\""|tr -d "\"")") done # Ensure the correct attribute if file exists FILE_CMD="chmod" for LOG_FILE_PATH in "${LOG_FILE_PATHS[@]}" do # Sanity check - if particular $LOG_FILE_PATH is empty string, skip it from further processing if [ -z "$LOG_FILE_PATH" ] then continue fi $FILE_CMD "0640" "$LOG_FILE_PATH" done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - Set rsyslog logfile configuration facts ansible.builtin.set_fact: rsyslog_etc_config: /etc/rsyslog.conf when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - Get IncludeConfig directive ansible.builtin.shell: | set -o pipefail grep -e '$IncludeConfig' {{ rsyslog_etc_config }} | cut -d ' ' -f 2 || true register: rsyslog_old_inc changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - Get include files directives ansible.builtin.shell: | set -o pipefail awk '/)/{f=0} /include\(/{f=1} f{ nf=gensub("^(include\\(|\\s*)file=\"(\\S+)\".*","\\2",1); if($0!=nf){ print nf }}' {{ rsyslog_etc_config }} || true register: rsyslog_new_inc changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - Aggregate rsyslog includes ansible.builtin.set_fact: include_config_output: '{{ rsyslog_old_inc.stdout_lines + rsyslog_new_inc.stdout_lines }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_old_inc is not skipped and rsyslog_new_inc is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - List all config files ansible.builtin.find: paths: '{{ item | dirname }}' patterns: '{{ item | basename }}' hidden: false follow: true loop: '{{ include_config_output | list + [rsyslog_etc_config] }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - include_config_output is defined register: rsyslog_config_files failed_when: false changed_when: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - Extract log files old format ansible.builtin.shell: | set -o pipefail grep -oP '^[^(\s|#|\$)]+[\s]+.*[\s]+-?(/+[^:;\s]+);*\.*$' {{ item.1.path }} | \ awk '{print $NF}' | \ sed -e 's/^-//' || true loop: '{{ rsyslog_config_files.results | default([]) | subelements(''files'') }}' register: log_files_old changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_config_files is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - Extract log files new format ansible.builtin.shell: | set -o pipefail grep -ozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" {{ item.1.path }} | \ grep -aoP "\bFile\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)" | \ grep -oE "\"([/[:alnum:][:punct:]]*)\"" | \ tr -d "\""|| true loop: '{{ rsyslog_config_files.results | default([]) | subelements(''files'') }}' register: log_files_new changed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslog_config_files is not skipped tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions - Sum all log files found ansible.builtin.set_fact: log_files: '{{ log_files_new.results | map(attribute=''stdout_lines'') | list | flatten | unique + log_files_old.results | map(attribute=''stdout_lines'') | list | flatten | unique }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions - name: Ensure System Log Files Have Correct Permissions -Setup log files attribute ansible.builtin.file: path: '{{ item }}' mode: '0640' state: file loop: '{{ log_files | list | flatten | unique }}' failed_when: false when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.1 - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_files_permissions Ensure remote access methods are monitored in Rsyslog Logging of remote access methods must be implemented to help identify cyber attacks and ensure ongoing compliance with remote access policies are being audited and upheld. An examples of a remote access method is the use of the Remote Desktop Protocol (RDP) from an external, non-organization controlled network. The /etc/rsyslog.conf or /etc/rsyslog.d/*.conf file should contain a match for the following selectors: auth.*, authpriv.*, and daemon.*. If not, use the following as an example configuration: auth.*;authpriv.* /var/log/secure daemon.* /var/log/messages AC-17(1) SRG-OS-000032-GPOS-00013 Logging remote access methods can be used to trace the decrease the risks associated with remote user access management. It can also be used to spot cyber attacks and ensure ongoing compliance with organizational policies surrounding the use of remote access methods. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && rpm --quiet -q rsyslog; then declare -A REMOTE_METHODS=( ['auth.*']='^[^#]*auth\.\*.*$' ['authpriv.*']='^[^#]*authpriv\.\*.*$' ['daemon.*']='^[^#]*daemon\.\*.*$' ) declare -A LOCATIONS=( ['auth.*']='/var/log/secure' ['authpriv.*']='/var/log/secure' ['daemon.*']='/var/log/messages' ) if [[ ! -f /etc/rsyslog.conf ]]; then # Something is not right, create the file touch /etc/rsyslog.conf fi # Loop through the remote methods associative array for K in "${!REMOTE_METHODS[@]}" do # Check to see if selector/value exists if ! grep -rq "${REMOTE_METHODS[$K]}" /etc/rsyslog.*; then APPEND_LINE=$(sed -rn "/^\S+\s+\${LOCATIONS[$K]}$/p" /etc/rsyslog.conf) # Make sure we have a line to insert after, otherwise append to end if [[ ! -z ${APPEND_LINE} ]]; then # Add selector to file sed -r -i "0,/^(\S+\s+\/var\/log\/secure$)/s//\1\n${K} \/var\/log\/secure/" /etc/rsyslog.conf else echo "${K} ${LOCATIONS[$K]}" >> /etc/rsyslog.conf fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-17(1) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_remote_access_monitoring - name: 'Ensure remote access methods are monitored in Rsyslog: Set facts' ansible.builtin.set_fact: conf_files: - /etc/rsyslog.conf remote_methods: - selector: auth.* regexp: ^.*auth\.\*.*$ location: /var/log/secure - selector: authpriv.* regexp: ^.*authpriv\.\*.*$ location: /var/log/secure - selector: daemon.* regexp: ^.*daemon\.\*.*$ location: /var/log/messages when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-17(1) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_remote_access_monitoring - name: 'Ensure remote access methods are monitored in Rsyslog: Ensure rsyslog.conf exists' ansible.builtin.file: path: '{{ conf_files.0 }}' state: touch access_time: preserve modification_time: preserve when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-17(1) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_remote_access_monitoring - name: 'Ensure remote access methods are monitored in Rsyslog: Gather conf.d files' ansible.builtin.find: patterns: - '*.conf' paths: - /etc/rsyslog.d register: rsyslogd when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-17(1) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_remote_access_monitoring - name: 'Ensure remote access methods are monitored in Rsyslog: Set conf file(s)' ansible.builtin.set_fact: conf_files: '{{ conf_files + [item.path] }}' loop: '{{ rsyslogd.files }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - rsyslogd.matched > 0 tags: - NIST-800-53-AC-17(1) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_remote_access_monitoring - name: 'Ensure remote access methods are monitored in Rsyslog: Check for existing values' ansible.builtin.lineinfile: path: '{{ item.1 }}' regexp: '{{ item.0.regexp }}' state: absent check_mode: true changed_when: false register: remote_method_values loop: '{{ remote_methods|product(conf_files)|list }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' tags: - NIST-800-53-AC-17(1) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_remote_access_monitoring - name: 'Ensure remote access methods are monitored in Rsyslog: Configure' ansible.builtin.lineinfile: path: /etc/rsyslog.conf line: '{{ item.item.0.selector }} {{ item.item.0.location }}' insertafter: ^.*\/var\/log\/secure.*$ create: true loop: '{{ remote_method_values.results }}' when: - '"kernel" in ansible_facts.packages' - '"rsyslog" in ansible_facts.packages' - item.found == 0 tags: - NIST-800-53-AC-17(1) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - rsyslog_remote_access_monitoring systemd-journald systemd-journald is a system service that collects and stores logging data. It creates and maintains structured, indexed journals based on logging information that is received from a variety of sources. For more information on systemd-journald and additional systemd-journald configuration options, see https://systemd.io/. Install systemd-journal-remote Package Journald (via systemd-journal-remote ) supports the ability to send log events it gathers to a remote log host or to receive messages from remote hosts, thus enabling centralised log management. SRG-OS-000479-GPOS-00224 6.2.2.1.1 Storing log data on a remote host protects log integrity from local attacks. If an attacker gains root access on the local system, they could tamper with or remove log data that is stored on the local system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "systemd-journal-remote" ; then dnf install -y "systemd-journal-remote" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_systemd-journal-remote_installed - name: Ensure systemd-journal-remote is installed ansible.builtin.package: name: systemd-journal-remote state: present when: '"kernel" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_systemd-journal-remote_installed include install_systemd-journal-remote class install_systemd-journal-remote { package { 'systemd-journal-remote': ensure => 'installed', } } package --add=systemd-journal-remote [[packages]] name = "systemd-journal-remote" version = "*" package install systemd-journal-remote dnf install systemd-journal-remote Enable systemd-journald Service The systemd-journald service is an essential component of systemd. The systemd-journald service can be enabled with the following command: $ sudo systemctl enable systemd-journald.service SC-24 SRG-OS-000269-GPOS-00103 6.2.1.1 In the event of a system failure, Fedora must preserve any information necessary to determine cause of failure and any information necessary to return to operations with least disruption to system processes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'systemd-journald.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'systemd-journald.service' fi "$SYSTEMCTL_EXEC" enable 'systemd-journald.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-SC-24 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_systemd-journald_enabled - name: Enable systemd-journald Service - Enable service systemd-journald block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable systemd-journald Service - Enable Service systemd-journald ansible.builtin.systemd: name: systemd-journald enabled: true state: started masked: false when: - '"systemd" in ansible_facts.packages' tags: - NIST-800-53-SC-24 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_systemd-journald_enabled - special_service_block when: '"kernel" in ansible_facts.packages' include enable_systemd-journald class enable_systemd-journald { service {'systemd-journald': enable => true, ensure => 'running', } } [customizations.services] enabled = ["systemd-journald"] service enable systemd-journald Ensure journald is configured to compress large log files The journald system can compress large log files to avoid fill the system disk. 6.2.2.3 Log files that are not properly compressed run the risk of growing so large that they fill up the log partition. Valuable logging information could be lost if the log partition becomes full. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/systemd/journald.conf.d/complianceascode_hardening.conf /etc/systemd/journald.conf.d/*.conf /etc/systemd/journald.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[Journal\]([^\n\[]*\n+)+?[[:space:]]*Compress" "$f"; then sed -i "s/Compress[^(\n)]*/Compress=yes/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[Journal\]" "$f"; then sed -i "/[[:space:]]*\[Journal\]/a Compress=yes" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/systemd/journald.conf.d/complianceascode_hardening.conf /etc/systemd/journald.conf.d/*.conf /etc/systemd/journald.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[Journal]\nCompress=yes" >> "$file" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - journald_compress - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to compress large log files - Search for a section in files ansible.builtin.find: paths: '{{item.path}}' patterns: '{{item.pattern}}' contains: ^\s*\[Journal\] read_whole_file: true use_regex: true register: systemd_dropin_files_with_section loop: - path: '{{ ''/etc/systemd/journald.conf'' | dirname }}' pattern: '{{ ''/etc/systemd/journald.conf'' | basename | regex_escape }}' - path: /etc/systemd/journald.conf.d pattern: .*\.conf when: '"kernel" in ansible_facts.packages' tags: - journald_compress - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to compress large log files - Count number of files which contain the correct section ansible.builtin.set_fact: count_of_systemd_dropin_files_with_section: '{{systemd_dropin_files_with_section.results | map(attribute=''matched'') | list | map(''int'') | sum}}' when: '"kernel" in ansible_facts.packages' tags: - journald_compress - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to compress large log files - Add missing configuration to correct section community.general.ini_file: path: '{{item}}' section: Journal option: Compress value: 'yes' state: present no_extra_spaces: true when: - '"kernel" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int > 0 loop: '{{systemd_dropin_files_with_section.results | sum(attribute=''files'', start=[]) | map(attribute=''path'') | list }}' tags: - journald_compress - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to compress large log files - Add configuration to new remediation file community.general.ini_file: path: /etc/systemd/journald.conf.d/complianceascode_hardening.conf section: Journal option: Compress value: 'yes' state: present no_extra_spaces: true create: true when: - '"kernel" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int == 0 tags: - journald_compress - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure journald is configured to write log files to persistent disk The journald system may store log files in volatile memory or locally on disk. If the logs are only stored in volatile memory they will be lost upon reboot. 6.2.2.4 Log files contain valuable data and need to be persistent to aid in possible investigations. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/systemd/journald.conf.d/complianceascode_hardening.conf /etc/systemd/journald.conf.d/*.conf /etc/systemd/journald.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[Journal\]([^\n\[]*\n+)+?[[:space:]]*Storage" "$f"; then sed -i "s/Storage[^(\n)]*/Storage=persistent/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[Journal\]" "$f"; then sed -i "/[[:space:]]*\[Journal\]/a Storage=persistent" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/systemd/journald.conf.d/complianceascode_hardening.conf /etc/systemd/journald.conf.d/*.conf /etc/systemd/journald.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[Journal]\nStorage=persistent" >> "$file" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - journald_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to write log files to persistent disk - Search for a section in files ansible.builtin.find: paths: '{{item.path}}' patterns: '{{item.pattern}}' contains: ^\s*\[Journal\] read_whole_file: true use_regex: true register: systemd_dropin_files_with_section loop: - path: '{{ ''/etc/systemd/journald.conf'' | dirname }}' pattern: '{{ ''/etc/systemd/journald.conf'' | basename | regex_escape }}' - path: /etc/systemd/journald.conf.d pattern: .*\.conf when: '"kernel" in ansible_facts.packages' tags: - journald_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to write log files to persistent disk - Count number of files which contain the correct section ansible.builtin.set_fact: count_of_systemd_dropin_files_with_section: '{{systemd_dropin_files_with_section.results | map(attribute=''matched'') | list | map(''int'') | sum}}' when: '"kernel" in ansible_facts.packages' tags: - journald_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to write log files to persistent disk - Add missing configuration to correct section community.general.ini_file: path: '{{item}}' section: Journal option: Storage value: persistent state: present no_extra_spaces: true when: - '"kernel" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int > 0 loop: '{{systemd_dropin_files_with_section.results | sum(attribute=''files'', start=[]) | map(attribute=''path'') | list }}' tags: - journald_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure journald is configured to write log files to persistent disk - Add configuration to new remediation file community.general.ini_file: path: /etc/systemd/journald.conf.d/complianceascode_hardening.conf section: Journal option: Storage value: persistent state: present no_extra_spaces: true create: true when: - '"kernel" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int == 0 tags: - journald_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Disable systemd-journal-remote Socket Journald supports the ability to receive messages from remote hosts, thus acting as a log server. Clients should not receive data from other hosts. NOTE: The same package, systemd-journal-remote , is used for both sending logs to remote hosts and receiving incoming logs. With regards to receiving logs, there are two Systemd unit files; systemd-journal-remote.socket and systemd-journal-remote.service. 6.2.2.1.4 If a client is configured to also receive data, thus turning it into a server, the client system is acting outside it's operational boundary. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SOCKET_NAME="systemd-journal-remote.socket" SYSTEMCTL_EXEC='/usr/bin/systemctl' if "$SYSTEMCTL_EXEC" -q list-unit-files --type socket | grep -q "$SOCKET_NAME"; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop "$SOCKET_NAME" fi "$SYSTEMCTL_EXEC" mask "$SOCKET_NAME" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - socket_systemd-journal-remote_disabled - name: Disable systemd-journal-remote Socket - Collect systemd Socket Units Present in the System ansible.builtin.command: cmd: systemctl -q list-unit-files --type socket register: result_systemd_unit_files changed_when: false when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - socket_systemd-journal-remote_disabled - name: Disable systemd-journal-remote Socket - Ensure systemd-journal-remote.socket is Masked ansible.builtin.systemd: name: systemd-journal-remote.socket state: stopped enabled: false masked: true when: - '"kernel" in ansible_facts.packages' - result_systemd_unit_files.stdout_lines is search("systemd-journal-remote.socket") tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - socket_systemd-journal-remote_disabled Ensure All Logs are Rotated by logrotate Edit the file /etc/logrotate.d/syslog. Find the first line, which should look like this (wrapped for clarity): /var/log/messages /var/log/secure /var/log/maillog /var/log/spooler \ /var/log/boot.log /var/log/cron { Edit this line so that it contains a one-space-separated listing of each log file referenced in /etc/rsyslog.conf. All logs in use on a system must be rotated regularly, or the log files will consume disk space over time, eventually interfering with system operation. The file /etc/logrotate.d/syslog is the configuration file used by the logrotate program to maintain all log files written by syslog. By default, it rotates logs weekly and stores four archival copies of each log. These settings can be modified by editing /etc/logrotate.conf, but the defaults are sufficient for purposes of this guide. Note that logrotate is run nightly by the cron job /etc/cron.daily/logrotate. If particularly active logs need to be rotated more often than once a day, some other mechanism must be used. Ensure logrotate is Installed logrotate is installed by default. The logrotate package can be installed with the following command: $ sudo dnf install logrotate 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) PR.PT-1 Req-10.7 R71 10.5.1 10.5 The logrotate package provides the logrotate services. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "logrotate" ; then dnf install -y "logrotate" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - PCI-DSSv4-10.5 - PCI-DSSv4-10.5.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_logrotate_installed - name: Ensure logrotate is installed ansible.builtin.package: name: logrotate state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - PCI-DSSv4-10.5 - PCI-DSSv4-10.5.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_logrotate_installed include install_logrotate class install_logrotate { package { 'logrotate': ensure => 'installed', } } package --add=logrotate [[packages]] name = "logrotate" version = "*" package install logrotate dnf install logrotate Ensure Logrotate Runs Periodically The logrotate utility allows for the automatic rotation of log files. The frequency of rotation is specified in /etc/logrotate.conf, which triggers a cron task or a timer. To configure logrotate to run daily, add or correct the following line in /etc/logrotate.conf: # rotate log files frequency daily 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) PR.PT-1 Req-10.7 R71 Log files that are not properly rotated run the risk of growing so large that they fill up the /var/log partition. Valuable logging information could be lost if the /var/log partition becomes full. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q logrotate; }; then LOGROTATE_CONF_FILE="/etc/logrotate.conf" if ! rpm -q --quiet "crontabs" ; then dnf install -y "crontabs" fi CRON_DAILY_LOGROTATE_FILE="/etc/cron.daily/logrotate" # daily rotation is configured grep -q "^daily$" $LOGROTATE_CONF_FILE|| echo "daily" >> $LOGROTATE_CONF_FILE # remove any line configuring weekly, monthly or yearly rotation sed -i '/^\s*\(weekly\|monthly\|yearly\).*$/d' $LOGROTATE_CONF_FILE # configure cron.daily if not already if ! grep -q "^[[:space:]]*/usr/sbin/logrotate[[:alnum:][:blank:][:punct:]]*$LOGROTATE_CONF_FILE$" $CRON_DAILY_LOGROTATE_FILE; then echo '#!/bin/sh' > $CRON_DAILY_LOGROTATE_FILE echo "/usr/sbin/logrotate $LOGROTATE_CONF_FILE" >> $CRON_DAILY_LOGROTATE_FILE fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - configure_strategy - ensure_logrotate_activated - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure daily log rotation in /etc/logrotate.conf ansible.builtin.lineinfile: create: true dest: /etc/logrotate.conf regexp: ^daily$ line: daily when: - '"kernel" in ansible_facts.packages' - '"logrotate" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - configure_strategy - ensure_logrotate_activated - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Make sure daily log rotation setting is not overriden in /etc/logrotate.conf ansible.builtin.lineinfile: create: false dest: /etc/logrotate.conf regexp: ^[\s]*(weekly|monthly|yearly)$ state: absent when: - '"kernel" in ansible_facts.packages' - '"logrotate" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - configure_strategy - ensure_logrotate_activated - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure cron.daily if not already block: - name: Add shebang ansible.builtin.lineinfile: path: /etc/cron.daily/logrotate line: '#!/bin/sh' insertbefore: BOF create: true - name: Add logrotate call ansible.builtin.lineinfile: path: /etc/cron.daily/logrotate line: /usr/sbin/logrotate /etc/logrotate.conf regexp: ^[\s]*/usr/sbin/logrotate[\s\S]*/etc/logrotate.conf$ create: true when: - '"kernel" in ansible_facts.packages' - '"logrotate" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - configure_strategy - ensure_logrotate_activated - low_complexity - low_disruption - medium_severity - no_reboot_needed --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%20see%20%22man%20logrotate%22%20for%20details%0A%23%20rotate%20log%20files%20daily%0Adaily%0A%0A%23%20keep%204%20weeks%20worth%20of%20backlogs%0Arotate%2030%0A%0A%23%20create%20new%20%28empty%29%20log%20files%20after%20rotating%20old%20ones%0Acreate%0A%0A%23%20use%20date%20as%20a%20suffix%20of%20the%20rotated%20file%0Adateext%0A%0A%23%20uncomment%20this%20if%20you%20want%20your%20log%20files%20compressed%0A%23compress%0A%0A%23%20RPM%20packages%20drop%20log%20rotation%20information%20into%20this%20directory%0Ainclude%20/etc/logrotate.d%0A%0A%23%20system-specific%20logs%20may%20be%20also%20be%20configured%20here. }} mode: 0644 path: /etc/logrotate.conf overwrite: true Configure rsyslogd to Accept Remote Messages If Acting as a Log Server By default, rsyslog does not listen over the network for log messages. If needed, modules can be enabled to allow the rsyslog daemon to receive messages from other systems and for the system thus to act as a log server. If the system is not a log server, then lines concerning these modules should remain commented out. Ensure syslog-ng is Installed syslog-ng can be installed in replacement of rsyslog. The syslog-ng-core package can be installed with the following command: $ sudo dnf install syslog-ng-core 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) PR.PT-1 The syslog-ng-core package provides the syslog-ng daemon, which provides system logging services. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "syslog-ng" ; then dnf install -y "syslog-ng" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_syslogng_installed - name: Ensure syslog-ng is installed ansible.builtin.package: name: syslog-ng state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_syslogng_installed include install_syslog-ng class install_syslog-ng { package { 'syslog-ng': ensure => 'installed', } } package --add=syslog-ng [[packages]] name = "syslog-ng" version = "*" package install syslog-ng dnf install syslog-ng Enable syslog-ng Service The syslog-ng service (in replacement of rsyslog) provides syslog-style logging by default on Debian. The syslog-ng service can be enabled with the following command: $ sudo systemctl enable syslog-ng.service 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO13.01 BAI03.05 BAI04.04 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 A.17.2.1 CM-6(a) AU-4(1) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.DS-4 PR.PT-1 The syslog-ng service must be running in order to provide logging services, which are essential to system administration. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'syslog-ng.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'syslog-ng.service' fi "$SYSTEMCTL_EXEC" enable 'syslog-ng.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-4(1) - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_syslogng_enabled - name: Enable syslog-ng Service - Enable service syslog-ng block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable syslog-ng Service - Enable Service syslog-ng ansible.builtin.systemd: name: syslog-ng enabled: true state: started masked: false when: - '"syslog-ng" in ansible_facts.packages' tags: - NIST-800-53-AU-4(1) - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_syslogng_enabled - special_service_block when: '"kernel" in ansible_facts.packages' include enable_syslog-ng class enable_syslog-ng { service {'syslog-ng': enable => true, ensure => 'running', } } [customizations.services] enabled = ["syslog-ng"] service enable syslog-ng Enable rsyslog to Accept Messages via TCP, if Acting As Log Server The rsyslog daemon should not accept remote messages unless the system acts as a log server. If the system needs to act as a central log server, add the following lines to /etc/rsyslog.conf to enable reception of messages over TCP: $ModLoad imtcp $InputTCPServerRun 514 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CIP-004-6 R2.2.2 CIP-004-6 R3.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R6.5 CM-6(a) AU-6(3) AU-6(4) PR.PT-1 If the system needs to act as a log server, this ensures that it can receive messages over a reliable TCP connection. Enable rsyslog to Accept Messages via UDP, if Acting As Log Server The rsyslog daemon should not accept remote messages unless the system acts as a log server. If the system needs to act as a central log server, add the following lines to /etc/rsyslog.conf to enable reception of messages over UDP: $ModLoad imudp $UDPServerRun 514 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CIP-004-6 R2.2.2 CIP-004-6 R3.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R6.5 CM-6(a) AU-6(3) AU-6(4) PR.PT-1 Many devices, such as switches, routers, and other Unix-like systems, may only support the traditional syslog transmission over UDP. If the system must act as a log server, this enables it to receive their messages as well. Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server The rsyslog daemon should not accept remote messages unless the system acts as a log server. To ensure that it is not listening on the network, ensure any of the following lines are not found in rsyslog configuration files. If using legacy syntax: $ModLoad imtcp $InputTCPServerRun port $ModLoad imudp $UDPServerRun port $ModLoad imrelp $InputRELPServerRun port If using RainerScript syntax: module(load="imtcp") module(load="imudp") input(type="imtcp" port="514") input(type="imudp" port="514") 1 11 12 13 14 15 16 18 3 4 5 6 8 9 APO01.06 APO11.04 APO13.01 BAI03.05 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.07 DSS06.02 MEA02.01 4.2.3.4 4.3.3.3.9 4.3.3.4 4.3.3.5.8 4.3.4.3.2 4.3.4.3.3 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 4.4.3.3 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 0988 1405 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.5.1 A.12.6.2 A.12.7.1 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) DE.AE-1 ID.AM-3 PR.AC-5 PR.DS-5 PR.IP-1 PR.PT-1 PR.PT-4 SRG-OS-000480-GPOS-00227 Any process which receives messages from the network incurs some risk of receiving malicious messages. This risk can be eliminated for rsyslog by configuring it not to listen on the network. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then legacy_regex='^\s*\$(((Input(TCP|RELP)|UDP)ServerRun)|ModLoad\s+(imtcp|imudp|imrelp))' rainer_regex='^\s*(module|input)\((load|type)="(imtcp|imudp)".*$' readarray -t legacy_targets < <(grep -l -E -r "${legacy_regex[@]}" /etc/rsyslog.conf /etc/rsyslog.d/) readarray -t rainer_targets < <(grep -l -E -r "${rainer_regex[@]}" /etc/rsyslog.conf /etc/rsyslog.d/) config_changed=false if [ ${#legacy_targets[@]} -gt 0 ]; then for target in "${legacy_targets[@]}"; do sed -E -i "/$legacy_regex/ s/^/# /" "$target" done config_changed=true fi if [ ${#rainer_targets[@]} -gt 0 ]; then for target in "${rainer_targets[@]}"; do sed -E -i "/$rainer_regex/ s/^/# /" "$target" done config_changed=true fi if $config_changed; then systemctl restart rsyslog.service fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Define Rsyslog Config Lines Regex in Legacy Syntax ansible.builtin.set_fact: rsyslog_listen_legacy_regex: ^\s*\$(((Input(TCP|RELP)|UDP)ServerRun)|ModLoad\s+(imtcp|imudp|imrelp)) when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Search for Legacy Config Lines in Rsyslog Main Config File ansible.builtin.find: paths: /etc pattern: rsyslog.conf contains: '{{ rsyslog_listen_legacy_regex }}' register: rsyslog_listen_legacy_main_file when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Search for Legacy Config Lines in Rsyslog Include Files ansible.builtin.find: paths: /etc/rsyslog.d/ pattern: '*.conf' contains: '{{ rsyslog_listen_legacy_regex }}' register: rsyslog_listen_legacy_include_files when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Assemble List of Config Files With Listen Lines in Legacy Syntax ansible.builtin.set_fact: rsyslog_legacy_remote_listen_files: '{{ rsyslog_listen_legacy_main_file.files | map(attribute=''path'') | list + rsyslog_listen_legacy_include_files.files | map(attribute=''path'') | list }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Comment Listen Config Lines Wherever Defined Using Legacy Syntax ansible.builtin.replace: path: '{{ item }}' regexp: '{{ rsyslog_listen_legacy_regex }}' replace: '# \1' loop: '{{ rsyslog_legacy_remote_listen_files }}' register: rsyslog_listen_legacy_comment when: - '"kernel" in ansible_facts.packages' - rsyslog_legacy_remote_listen_files | length > 0 tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Define Rsyslog Config Lines Regex in RainerScript Syntax ansible.builtin.set_fact: rsyslog_listen_rainer_regex: ^\s*(module|input)\((load|type)="(imtcp|imudp)".*$ when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Search for RainerScript Config Lines in Rsyslog Main Config File ansible.builtin.find: paths: /etc pattern: rsyslog.conf contains: '{{ rsyslog_listen_rainer_regex }}' register: rsyslog_rainer_remote_main_file when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Search for RainerScript Config Lines in Rsyslog Include Files ansible.builtin.find: paths: /etc/rsyslog.d/ pattern: '*.conf' contains: '{{ rsyslog_listen_rainer_regex }}' register: rsyslog_rainer_remote_include_files when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Assemble List of Config Files With Listen Lines in RainerScript ansible.builtin.set_fact: rsyslog_rainer_remote_listen_files: '{{ rsyslog_rainer_remote_main_file.files | map(attribute=''path'') | list + rsyslog_rainer_remote_include_files.files | map(attribute=''path'') | list }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Comment Listen Config Lines Wherever Defined Using RainerScript ansible.builtin.replace: path: '{{ item }}' regexp: '{{ rsyslog_listen_rainer_regex }}' replace: '# \1' loop: '{{ rsyslog_rainer_remote_listen_files }}' register: rsyslog_listen_rainer_comment when: - '"kernel" in ansible_facts.packages' - rsyslog_rainer_remote_listen_files | length > 0 tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server - Restart Rsyslog if Any Line Were Commented Out ansible.builtin.service: name: rsyslog state: restarted when: - '"kernel" in ansible_facts.packages' - rsyslog_listen_legacy_comment is changed or rsyslog_listen_rainer_comment is changed tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_nolisten Rsyslog Logs Sent To Remote Host If system logs are to be useful in detecting malicious activities, it is necessary to send logs to a remote server. An intruder who has compromised the root account on a system may delete the log entries which indicate that the system was attacked before they are seen by an administrator. However, it is recommended that logs be stored on the local host in addition to being sent to the loghost, especially if rsyslog has been configured to use the UDP protocol to send messages over a network. UDP does not guarantee reliable delivery, and moderately busy sites will lose log messages occasionally, especially in periods of high traffic which may be the result of an attack. In addition, remote rsyslog messages are not authenticated in any way by default, so it is easy for an attacker to introduce spurious messages to the central log server. Also, some problems cause loss of network connectivity, which will prevent the sending of messages to the central server. For all of these reasons, it is better to store log messages both centrally and on each host, so that they can be correlated if necessary. Remote Log Server Specify an URI or IP address of a remote host where the log messages will be sent and stored. logcollector Ensure Logs Sent To Remote Host To configure rsyslog to send logs to a remote log server, open /etc/rsyslog.conf and read and understand the last section of the file, which describes the multiple directives necessary to activate remote logging. Along with these other directives, the system can be configured to forward its logs to a particular log server by adding or correcting one of the following lines, substituting appropriately. The choice of protocol depends on the environment of the system; although TCP and RELP provide more reliable message delivery, they may not be supported in all environments. To use UDP for log message delivery: *.* @ Or in RainerScript: *.* action(type="omfwd" ... target="" protocol="udp") To use TCP for log message delivery: *.* @@ Or in RainerScript: *.* action(type="omfwd" ... target="" protocol="tcp") To use RELP for log message delivery: *.* :omrelp: Or in RainerScript: *.* action(type="omfwd" ... target="" protocol="relp") There must be a resolvable DNS CNAME or Alias record set to "" for logs to be sent correctly to the centralized logging utility. It is important to configure queues in case the client is sending log messages to a remote server. If queues are not configured, the system will stop functioning when the connection to the remote server is not available. Please consult Rsyslog documentation for more information about configuration of queues. The example configuration which should go into /etc/rsyslog.conf can look like the following lines: $ActionQueueType LinkedList $ActionQueueFileName queuefilename $ActionQueueMaxDiskSpace 1g $ActionQueueSaveOnShutdown on $ActionResumeRetryCount -1 Or if using Rainer Script syntax, it could be: *.* action(type="omfwd" queue.type="linkedlist" queue.filename="example_fwd" action.resumeRetryCount="-1" queue.saveOnShutdown="on" target="example.com" port="30514" protocol="tcp") 1 13 14 15 16 2 3 5 6 APO11.04 APO13.01 BAI03.05 BAI04.04 DSS05.04 DSS05.07 MEA02.01 164.308(a)(1)(ii)(D) 164.308(a)(5)(ii)(B) 164.308(a)(5)(ii)(C) 164.308(a)(6)(ii) 164.308(a)(8) 164.310(d)(2)(iii) 164.312(b) 164.314(a)(2)(i)(C) 164.314(a)(2)(iii) 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 7.1 SR 7.2 0988 1405 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.17.2.1 CIP-003-8 R5.2 CIP-004-6 R3.3 CM-6(a) AU-4(1) AU-9(2) PR.DS-4 PR.PT-1 SRG-OS-000479-GPOS-00224 SRG-OS-000480-GPOS-00227 SRG-OS-000342-GPOS-00133 R71 A log server (loghost) receives syslog messages from one or more systems. This data can be used as an additional log source in the event a system is compromised and its local logs are suspect. Forwarding log messages to a remote loghost also provides system administrators with a centralized place to view the status of multiple hosts within the enterprise. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then rsyslog_remote_loghost_address='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^\*\.\*") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "@@$rsyslog_remote_loghost_address" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^\*\.\*\\>" "/etc/rsyslog.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^\*\.\*\\>.*/$escaped_formatted_output/gi" "/etc/rsyslog.conf" else if [[ -s "/etc/rsyslog.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/rsyslog.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/rsyslog.conf" fi printf '%s\n' "$formatted_output" >> "/etc/rsyslog.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-4(1) - NIST-800-53-AU-9(2) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - rsyslog_remote_loghost - name: XCCDF Value rsyslog_remote_loghost_address # promote to variable set_fact: rsyslog_remote_loghost_address: !!str tags: - always - name: Set rsyslog remote loghost ansible.builtin.lineinfile: dest: /etc/rsyslog.conf regexp: ^\*\.\* line: '*.* @@{{ rsyslog_remote_loghost_address }}' create: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-4(1) - NIST-800-53-AU-9(2) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - rsyslog_remote_loghost Configure TLS for rsyslog remote logging Configure rsyslog to use Transport Layer Security (TLS) support for logging to remote server for the Forwarding Output Module in /etc/rsyslog.conf using action. You can use the following command: echo 'action(type="omfwd" protocol="tcp" Target="<remote system>" port="6514" StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name" streamdriver.CheckExtendedKeyPurpose="on")' >> /etc/rsyslog.conf Replace the <remote system> in the above command with an IP address or a host name of the remote logging server. 0988 1405 AU-9(3) CM-6(a) SRG-OS-000480-GPOS-00227 SRG-OS-000120-GPOS-00061 R71 For protection of data being logged, the connection to the remote logging server needs to be authenticated and encrypted. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then rsyslog_remote_loghost_address='' params_to_add_if_missing=("protocol" "target" "port" "StreamDriver" "StreamDriverMode" "StreamDriverAuthMode" "streamdriver.CheckExtendedKeyPurpose") values_to_add_if_missing=("tcp" "$rsyslog_remote_loghost_address" "6514" "gtls" "1" "x509/name" "on") params_to_replace_if_wrong_value=("protocol" "StreamDriver" "StreamDriverMode" "StreamDriverAuthMode" "streamdriver.CheckExtendedKeyPurpose") values_to_replace_if_wrong_value=("tcp" "gtls" "1" "x509/name" "on") files_containing_omfwd=("$(grep -ilE '^[^#]*\s*action\s*\(\s*type\s*=\s*"omfwd".*' /etc/rsyslog.conf /etc/rsyslog.d/*.conf)") if [ -n "${files_containing_omfwd[*]}" ]; then for file in "${files_containing_omfwd[@]}"; do for ((i=0; i<${#params_to_replace_if_wrong_value[@]}; i++)); do sed -i -E -e 'H;$!d;x;s/^\n//' -e "s|(\s*action\s*\(\s*type\s*=\s*[\"]omfwd[\"].*?)${params_to_replace_if_wrong_value[$i]}\s*=\s*[\"]\S*[\"](.*\))|\1${params_to_replace_if_wrong_value[$i]}=\"${values_to_replace_if_wrong_value[$i]}\"\2|gI" "$file" done for ((i=0; i<${#params_to_add_if_missing[@]}; i++)); do if ! grep -qPzi "(?s)\s*action\s*\(\s*type\s*=\s*[\"]omfwd[\"].*?${params_to_add_if_missing[$i]}.*?\).*" "$file"; then sed -i -E -e 'H;$!d;x;s/^\n//' -e "s|(\s*action\s*\(\s*type\s*=\s*[\"]omfwd[\"])|\1\n${params_to_add_if_missing[$i]}=\"${values_to_add_if_missing[$i]}\"|gI" "$file" fi done done else echo "action(type=\"omfwd\" protocol=\"tcp\" Target=\"$rsyslog_remote_loghost_address\" port=\"6514\" StreamDriver=\"gtls\" StreamDriverMode=\"1\" StreamDriverAuthMode=\"x509/name\" streamdriver.CheckExtendedKeyPurpose=\"on\")" >> /etc/rsyslog.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: XCCDF Value rsyslog_remote_loghost_address # promote to variable set_fact: rsyslog_remote_loghost_address: !!str tags: - always - name: 'Configure TLS for rsyslog remote logging: search for omfwd action directive in rsyslog include files' ansible.builtin.find: paths: /etc/rsyslog.d/ pattern: '*.conf' contains: ^\s*action\s*\(\s*type\s*=\s*"omfwd".* register: rsyslog_includes_with_directive when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: search for omfwd action directive in rsyslog main config file' ansible.builtin.find: paths: /etc pattern: rsyslog.conf contains: ^\s*action\s*\(\s*type\s*=\s*"omfwd".* register: rsyslog_main_file_with_directive when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option parameters to be inserted if entirely missing' ansible.builtin.set_fact: rsyslog_parameters_to_add_if_missing: - protocol - target - port - StreamDriver - StreamDriverMode - StreamDriverAuthMode - streamdriver.CheckExtendedKeyPurpose when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option values to be inserted if entirely missing' ansible.builtin.set_fact: rsyslog_values_to_add_if_missing: - tcp - '{{ rsyslog_remote_loghost_address }}' - '6514' - gtls - '1' - x509/name - 'on' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option parameters to be replaced if defined with wrong values' ansible.builtin.set_fact: rsyslog_parameters_to_replace_if_wrong_value: - protocol - StreamDriver - StreamDriverMode - StreamDriverAuthMode - streamdriver.CheckExtendedKeyPurpose when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option values to be replaced when having wrong value' ansible.builtin.set_fact: rsyslog_values_to_replace_if_wrong_value: - tcp - gtls - '1' - x509/name - 'on' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: assemble list of files with existing directives' ansible.builtin.set_fact: rsyslog_files: '{{ rsyslog_includes_with_directive.files | map(attribute=''path'') | list + rsyslog_main_file_with_directive.files | map(attribute=''path'') | list }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: try to fix existing directives' block: - name: 'Configure TLS for rsyslog remote logging: Fix existing omfwd directives by adjusting the value' ansible.builtin.replace: path: '{{ item[0] }}' regexp: (?i)^(\s*action\s*\(\s*type\s*=\s*"omfwd"[\s\S]*)({{ item[1][0] | regex_escape() }}\s*=\s*"\S*")([\s\S]*\))$ replace: \1{{ item[1][0] }}="{{ item[1][1] }}"\3 loop: '{{ rsyslog_files | product (rsyslog_parameters_to_replace_if_wrong_value | zip(rsyslog_values_to_replace_if_wrong_value)) | list }}' - name: 'Configure TLS for rsyslog remote logging: Fix existing omfwd directives by adding parameter and value' ansible.builtin.replace: path: '{{ item[0] }}' regexp: (?i)^(\s*action\s*\(\s*type\s*=\s*"omfwd"(?:[\s\S](?!{{ item[1][0] | regex_escape() }}))*.)(\))$ replace: \1 {{ item[1][0] }}="{{ item[1][1] }}" \2 loop: '{{ rsyslog_files | product (rsyslog_parameters_to_add_if_missing | zip(rsyslog_values_to_add_if_missing)) | list }}' when: - '"kernel" in ansible_facts.packages' - rsyslog_includes_with_directive.matched or rsyslog_main_file_with_directive.matched tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls - name: 'Configure TLS for rsyslog remote logging: Add missing rsyslog directive' ansible.builtin.lineinfile: dest: /etc/rsyslog.conf line: action(type="omfwd" protocol="tcp" Target="{{ rsyslog_remote_loghost_address }}" port="6514" StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name" streamdriver.CheckExtendedKeyPurpose="on") create: true when: - '"kernel" in ansible_facts.packages' - not rsyslog_includes_with_directive.matched and not rsyslog_main_file_with_directive.matched tags: - NIST-800-53-AU-9(3) - NIST-800-53-CM-6(a) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - rsyslog_remote_tls Configure CA certificate for rsyslog remote logging Configure CA certificate for rsyslog logging to remote server using Transport Layer Security (TLS) using correct path for the DefaultNetstreamDriverCAFile global option in /etc/rsyslog.conf, for example with the following command: echo 'global(DefaultNetstreamDriverCAFile="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem")' >> /etc/rsyslog.conf Replace the /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem in the above command with the path to the file with CA certificate generated for the purpose of remote logging. Automatic remediation is not available as each organization has unique requirements. 0988 1405 SRG-OS-000480-GPOS-00227 R71 The CA certificate needs to be set or rsyslog.service fails to start with error: ca certificate is not set, cannot continue Network Configuration and Firewalls Most systems must be connected to a network of some sort, and this brings with it the substantial risk of network attack. This section discusses the security impact of decisions about networking which must be made when configuring a system. This section also discusses firewalls, network access controls, and other network security frameworks, which allow system-level rules to be written that can limit an attackers' ability to connect to your system. These rules can specify that network traffic should be allowed or denied from certain IP addresses, hosts, and networks. The rules can also specify which of the system's network services are available to particular hosts or networks. Disable Zeroconf Networking Zeroconf networking allows the system to assign itself an IP address and engage in IP communication without a statically-assigned address or even a DHCP server. Automatic address assignment via Zeroconf (or DHCP) is not recommended. To disable Zeroconf automatic route assignment in the 169.254.0.0 subnet, add or correct the following line in /etc/sysconfig/network: NOZEROCONF=yes 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Zeroconf addresses are in the network 169.254.0.0. The networking scripts add entries to the system's routing table for these addresses. Zeroconf address assignment commonly occurs when the system is configured to use DHCP but fails to receive an address assignment from the DHCP server. Prevent non-Privileged Users from Modifying Network Interfaces using nmcli By default, non-privileged users are given permissions to modify networking interfaces and configurations using the nmcli command. Non-privileged users should not be making configuration changes to network configurations. To ensure that non-privileged users do not have permissions to make changes to the network configuration using nmcli, create the following configuration in /etc/polkit-1/localauthority/20-org.d/10-nm-harden-access.pkla: [Disable General User Access to NetworkManager] Identity=default Action=org.freedesktop.NetworkManager.* ResultAny=no ResultInactive=no ResultActive=auth_admin 3.1.16 0418 1055 1402 AC-18(4) CM-6(a) 1.2.8 1.2 Allowing non-privileged users to make changes to network settings can allow untrusted access, prevent system availability, and/or can lead to a compromise or attack. # Remediation is applicable only in certain platforms if rpm --quiet -q polkit; then if ! rpm -q --quiet "polkit-pkla-compat" ; then dnf install -y "polkit-pkla-compat" fi printf "[Disable General User Access to NetworkManager]\nIdentity=default\nAction=org.freedesktop.NetworkManager.*\nResultAny=no\nResultInactive=no\nResultActive=auth_admin\n" > /etc/polkit-1/localauthority/20-org.d/10-nm-harden-access.pkla else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(4) - NIST-800-53-CM-6(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - low_complexity - low_disruption - medium_severity - network_nmcli_permissions - no_reboot_needed - restrict_strategy - name: Prevent non-Privileged Users from Modifying Network Interfaces using nmcli - Ensure polkit-pkla-compat is installed ansible.builtin.package: name: polkit-pkla-compat state: present when: '"polkit" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(4) - NIST-800-53-CM-6(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - low_complexity - low_disruption - medium_severity - network_nmcli_permissions - no_reboot_needed - restrict_strategy - name: Prevent non-Privileged Users from Modifying Network Interfaces using nmcli - Ensure non-privileged users do not have access to nmcli community.general.ini_file: path: /etc/polkit-1/localauthority/20-org.d/10-nm-harden-access.pkla section: Disable General User Access to NetworkManager option: '{{ item.option }}' value: '{{ item.value }}' no_extra_spaces: true create: true loop: - option: Identity value: default - option: Action value: org.freedesktop.NetworkManager.* - option: ResultAny value: 'no' - option: ResultInactive value: 'no' - option: ResultActive value: auth_admin when: '"polkit" in ansible_facts.packages' tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(4) - NIST-800-53-CM-6(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.8 - low_complexity - low_disruption - medium_severity - network_nmcli_permissions - no_reboot_needed - restrict_strategy Ensure System is Not Acting as a Network Sniffer The system should not be acting as a network sniffer, which can capture all traffic on the network to which it is connected. Run the following to determine if any interface is running in promiscuous mode: $ ip link | grep PROMISC Promiscuous mode of an interface can be disabled with the following command: $ sudo ip link set dev device_name multicast off promisc off 1 11 14 3 9 APO11.06 APO12.06 BAI03.10 BAI09.01 BAI09.02 BAI09.03 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.05 DSS04.05 DSS05.02 DSS05.05 DSS06.06 4.2.3.4 4.3.3.3.7 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 SR 7.8 A.11.1.2 A.11.2.4 A.11.2.5 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.16.1.6 A.8.1.1 A.8.1.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) CM-7(2) MA-3 DE.DP-5 ID.AM-1 PR.IP-1 PR.MA-1 PR.PT-3 SRG-OS-000480-GPOS-00227 1.4.5 1.4 Network interfaces in promiscuous mode allow for the capture of all network traffic visible to the system. If unauthorized individuals can access these applications, it may allow them to collect information such as logon IDs, passwords, and key exchanges between systems. If the system is being used to perform a network troubleshooting function, the use of these tools must be documented with the Information Systems Security Manager (ISSM) and restricted to only authorized personnel. # Remediation is applicable only in certain platforms if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then for interface in $(ip -o link show | cut -d ":" -f 2); do ip link set dev $interface multicast off promisc off done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Ensure System is Not Acting as a Network Sniffer - Gather network interfaces ansible.builtin.command: cmd: ip -o link show register: network_interfaces when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"] tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(2) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MA-3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - low_complexity - low_disruption - medium_severity - network_sniffer_disabled - no_reboot_needed - restrict_strategy - name: Ensure System is Not Acting as a Network Sniffer - Disable promiscuous mode ansible.builtin.command: cmd: ip link set dev {{ item.split(':')[1] }} multicast off promisc off loop: '{{ network_interfaces.stdout_lines }}' when: - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"] - network_interfaces.stdout_lines is defined and "item.split(':') | length == 3" tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(2) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MA-3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - low_complexity - low_disruption - medium_severity - network_sniffer_disabled - no_reboot_needed - restrict_strategy firewalld The dynamic firewall daemon firewalld provides a dynamically managed firewall with support for network “zones” to assign a level of trust to a network and its associated connections and interfaces. It has support for IPv4 and IPv6 firewall settings. It supports Ethernet bridges and has a separation of runtime and permanent configuration options. It also has an interface for services or applications to add firewall rules directly. A graphical configuration tool, firewall-config, is used to configure firewalld, which in turn uses iptables tool to communicate with Netfilter in the kernel which implements packet filtering. The firewall service provided by firewalld is dynamic rather than static because changes to the configuration can be made at anytime and are immediately implemented. There is no need to save or apply the changes. No unintended disruption of existing network connections occurs as no part of the firewall has to be reloaded. Inspect and Activate Default firewalld Rules Firewalls can be used to separate networks into different zones based on the level of trust the user has decided to place on the devices and traffic within that network. NetworkManager informs firewalld to which zone an interface belongs. An interface's assigned zone can be changed by NetworkManager or via the firewall-config tool. The zone settings in /etc/firewalld/ are a range of preset settings which can be quickly applied to a network interface. These are the zones provided by firewalld sorted according to the default trust level of the zones from untrusted to trusted: dropAny incoming network packets are dropped, there is no reply. Only outgoing network connections are possible.blockAny incoming network connections are rejected with an icmp-host-prohibited message for IPv4 and icmp6-adm-prohibited for IPv6. Only network connections initiated from within the system are possible.publicFor use in public areas. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted.externalFor use on external networks with masquerading enabled especially for routers. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted.dmzFor computers in your demilitarized zone that are publicly-accessible with limited access to your internal network. Only selected incoming connections are accepted.workFor use in work areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.homeFor use in home areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.internalFor use on internal networks. You mostly trust the other computers on the networks to not harm your computer. Only selected incoming connections are accepted.trustedAll network connections are accepted. It is possible to designate one of these zones to be the default zone. When interface connections are added to NetworkManager, they are assigned to the default zone. On installation, the default zone in firewalld is set to be the public zone. To find out all the settings of a zone, for example the public zone, enter the following command as root: # firewall-cmd --zone=public --list-all Example output of this command might look like the following: # firewall-cmd --zone=public --list-all public interfaces: services: mdns dhcpv6-client ssh ports: forward-ports: icmp-blocks: source-quench To view the network zones currently active, enter the following command as root: # firewall-cmd --get-service The following listing displays the result of this command on common Fedora system: # firewall-cmd --get-service amanda-client bacula bacula-client dhcp dhcpv6 dhcpv6-client dns ftp high-availability http https imaps ipp ipp-client ipsec kerberos kpasswd ldap ldaps libvirt libvirt-tls mdns mountd ms-wbt mysql nfs ntp openvpn pmcd pmproxy pmwebapi pmwebapis pop3s postgresql proxy-dhcp radius rpc-bind samba samba-client smtp ssh telnet tftp tftp-client transmission-client vnc-server wbem-https Finally to view the network zones that will be active after the next firewalld service reload, enter the following command as root: # firewall-cmd --get-service --permanent Install firewalld Package The firewalld package can be installed with the following command: $ sudo dnf install firewalld CM-6(a) FMT_SMF_EXT.1 SRG-OS-000096-GPOS-00050 SRG-OS-000297-GPOS-00115 SRG-OS-000298-GPOS-00116 SRG-OS-000480-GPOS-00227 SRG-OS-000480-GPOS-00232 4.1.2 1.2.1 1.2 "Firewalld" provides an easy and effective way to block/limit remote access to the system via ports, services, and protocols. Remote access services, such as those providing remote access to network devices and information systems, which lack automated control capabilities, increase risk and make remote user access management difficult at best. Remote access is access to nonpublic information systems by an authorized user (or an information system) communicating through an external, non-organization-controlled network. Remote access methods include, for example, dial-up, broadband, and wireless. Fedora functionality (e.g., SSH) must be capable of taking enforcement action if the audit reveals unauthorized activity. Automated control of remote access sessions allows organizations to ensure ongoing compliance with remote access policies by enforcing connection rules of remote access applications on a variety of information system components (e.g., servers, workstations, notebook computers, smartphones, and tablets)." # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "firewalld" ; then dnf install -y "firewalld" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_firewalld_installed - name: Ensure firewalld is installed ansible.builtin.package: name: firewalld state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_firewalld_installed include install_firewalld class install_firewalld { package { 'firewalld': ensure => 'installed', } } package --add=firewalld [[packages]] name = "firewalld" version = "*" package install firewalld dnf install firewalld Verify firewalld Enabled The firewalld service can be enabled with the following command: $ sudo systemctl enable firewalld.service 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.3 3.4.7 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CIP-003-8 R4 CIP-003-8 R5 CIP-004-6 R3 AC-4 CM-7(b) CA-3(5) SC-7(21) CM-6(a) PR.IP-1 FMT_SMF_EXT.1 SRG-OS-000096-GPOS-00050 SRG-OS-000297-GPOS-00115 SRG-OS-000480-GPOS-00227 SRG-OS-000480-GPOS-00231 SRG-OS-000480-GPOS-00232 SYS.1.6.A5 SYS.1.6.A21 4.1.2 1.2.1 1.2 Access control methods provide the ability to enhance system security posture by restricting services and known good IP addresses and address ranges. This prevents connections from unknown hosts and protocols. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q firewalld; }; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'firewalld.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'firewalld.service' fi "$SYSTEMCTL_EXEC" enable 'firewalld.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.3 - NIST-800-171-3.4.7 - NIST-800-53-AC-4 - NIST-800-53-CA-3(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(21) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_firewalld_enabled - name: Verify firewalld Enabled - Enable service firewalld block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Verify firewalld Enabled - Enable Service firewalld ansible.builtin.systemd: name: firewalld enabled: true state: started masked: false when: - '"firewalld" in ansible_facts.packages' tags: - NIST-800-171-3.1.3 - NIST-800-171-3.4.7 - NIST-800-53-AC-4 - NIST-800-53-CA-3(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(21) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_firewalld_enabled - special_service_block when: - '"kernel" in ansible_facts.packages' - '"firewalld" in ansible_facts.packages' include enable_firewalld class enable_firewalld { service {'firewalld': enable => true, ensure => 'running', } } [customizations.services] enabled = ["firewalld"] service enable firewalld Strengthen the Default Ruleset The default rules can be strengthened. The system scripts that activate the firewall rules expect them to be defined in configuration files under the /etc/firewalld/services and /etc/firewalld/zones directories. The following recommendations describe how to strengthen the default ruleset configuration file. An alternative to editing this configuration file is to create a shell script that makes calls to the firewall-cmd program to load in rules under the /etc/firewalld/services and /etc/firewalld/zones directories. Instructions apply to both unless otherwise noted. Language and address conventions for regular firewalld rules are used throughout this section. The program firewall-config allows additional services to penetrate the default firewall rules and automatically adjusts the firewalld ruleset(s). Configure Firewalld to Restrict Loopback Traffic Configure firewalld to restrict loopback traffic to the lo interface. The loopback traffic must be trusted by assigning the lo interface to the firewalld trusted zone. However, the loopback traffic must be restricted to the loopback interface as an anti-spoofing measure. To configure firewalld to restrict loopback traffic to the lo interface, run the following commands: sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv4 source address="127.0.0.1" destination not address="127.0.0.1" drop' sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv6 source address="::1" destination not address="::1" drop' To ensure firewalld settings are applied in runtime, run the following command: firewall-cmd --reload 4.2.2 1.4.1 1.4 Loopback traffic is generated between processes on machine and is typically critical to operation of the system. The loopback interface is the only place that loopback network traffic should be seen, all other interfaces should ignore traffic on this network as an anti-spoofing measure. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "firewalld" ; then dnf install -y "firewalld" fi ipv4_rule='rule family=ipv4 source address="127.0.0.1" destination not address="127.0.0.1" drop' ipv6_rule='rule family=ipv6 source address="::1" destination not address="::1" drop' if test "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" || { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; }; then firewall-offline-cmd --zone=trusted --add-rich-rule="${ipv4_rule}" firewall-offline-cmd --zone=trusted --add-rich-rule="${ipv6_rule}" elif systemctl is-active firewalld; then firewall-cmd --permanent --zone=trusted --add-rich-rule="${ipv4_rule}" firewall-cmd --permanent --zone=trusted --add-rich-rule="${ipv6_rule}" firewall-cmd --reload else echo " firewalld service is not active. Remediation aborted! This remediation could not be applied because it depends on firewalld service running. The service is not started by this remediation in order to prevent connection issues." fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_restricted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld Package is Installed ansible.builtin.package: name: '{{ item }}' state: present with_items: - firewalld when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_restricted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Restrict Loopback Traffic - Collect Facts About System Services ansible.builtin.service_facts: null register: result_services_states when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_restricted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Restrict Loopback Traffic - Remediation is Applicable if firewalld Service is Running block: - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld trusted Zone Restricts IPv4 Loopback Traffic ansible.builtin.command: cmd: firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv4 source address="127.0.0.1" destination not address="127.0.0.1" drop' register: result_trusted_ipv4_restriction changed_when: - '''ALREADY_ENABLED'' not in result_trusted_ipv4_restriction.stderr' - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld trusted Zone Restricts IPv6 Loopback Traffic ansible.builtin.command: cmd: firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv6 source address="::1" destination not address="::1" drop' register: result_trusted_ipv6_restriction changed_when: - '''ALREADY_ENABLED'' not in result_trusted_ipv6_restriction.stderr' - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld Changes are Applied ansible.builtin.service: name: firewalld state: reloaded when: - result_trusted_ipv4_restriction is changed or result_trusted_ipv6_restriction is changed when: - '"kernel" in ansible_facts.packages' - ansible_facts.services['firewalld.service'].state == 'running' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_restricted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Restrict Loopback Traffic - Informative Message Based on Service State ansible.builtin.assert: that: - ansible_check_mode or ansible_facts.services['firewalld.service'].state == 'running' fail_msg: - firewalld service is not active. Remediation aborted! - This remediation could not be applied because it depends on firewalld service running. - The service is not started by this remediation in order to prevent connection issues. success_msg: - Configure Firewalld to Restrict Loopback Traffic remediation successfully executed when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_restricted - low_complexity - low_disruption - medium_severity - no_reboot_needed Configure Firewalld to Trust Loopback Traffic Assign loopback interface to the firewalld trusted zone in order to explicitly allow the loopback traffic in the system. To configure firewalld to trust loopback traffic, run the following command: sudo firewall-cmd --permanent --zone=trusted --add-interface=lo To ensure firewalld settings are applied in runtime, run the following command: firewall-cmd --reload 4.2.2 1.4.1 1.4 Loopback traffic is generated between processes on machine and is typically critical to operation of the system. The loopback interface is the only place that loopback network traffic should be seen, all other interfaces should ignore traffic on this network as an anti-spoofing measure. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "firewalld" ; then dnf install -y "firewalld" fi if test "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" || { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; }; then firewall-offline-cmd --zone=trusted --add-interface=lo elif systemctl is-active firewalld; then firewall-cmd --permanent --zone=trusted --add-interface=lo firewall-cmd --reload else echo " firewalld service is not active. Remediation aborted! This remediation could not be applied because it depends on firewalld service running. The service is not started by this remediation in order to prevent connection issues." fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_trusted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Trust Loopback Traffic - Ensure firewalld Package is Installed ansible.builtin.package: name: '{{ item }}' state: present with_items: - firewalld when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_trusted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Trust Loopback Traffic - Collect Facts About System Services ansible.builtin.service_facts: null when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_trusted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Trust Loopback Traffic - Remediation is Applicable if firewalld Service is Running block: - name: Configure Firewalld to Trust Loopback Traffic - Ensure firewalld trusted Zone Includes lo Interface ansible.builtin.command: cmd: firewall-cmd --permanent --zone=trusted --add-interface=lo register: result_lo_interface_assignment changed_when: - '''ALREADY_ENABLED'' not in result_lo_interface_assignment.stderr' - name: Configure Firewalld to Trust Loopback Traffic - Ensure firewalld Changes are Applied ansible.builtin.service: name: firewalld state: reloaded when: - result_lo_interface_assignment is changed when: - '"kernel" in ansible_facts.packages' - ansible_facts.services['firewalld.service'].state == 'running' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_trusted - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure Firewalld to Trust Loopback Traffic - Informative Message Based on Service State ansible.builtin.assert: that: - ansible_check_mode or ansible_facts.services['firewalld.service'].state == 'running' fail_msg: - firewalld service is not active. Remediation aborted! - This remediation could not be applied because it depends on firewalld service running. - The service is not started by this remediation in order to prevent connection issues. success_msg: - Configure Firewalld to Trust Loopback Traffic remediation successfully executed when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.1 - configure_strategy - firewalld_loopback_traffic_trusted - low_complexity - low_disruption - medium_severity - no_reboot_needed Set Default firewalld Zone for Incoming Packets To set the default zone to drop for the built-in default zone which processes incoming IPv4 and IPv6 packets, modify the following line in /etc/firewalld/firewalld.conf to be: DefaultZone=drop To prevent denying any access to the system, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. 11 14 3 9 5.10.1 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.3 3.4.7 3.13.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 1416 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CA-3(5) CM-7(b) SC-7(23) CM-6(a) PR.IP-1 PR.PT-3 Req-1.4 SRG-OS-000480-GPOS-00227 1.3.1 1.3 In firewalld the default zone is applied only after all the applicable rules in the table are examined for a match. Setting the default zone to drop implements proper design for a firewall, i.e. any packets which are not explicitly permitted should not be accepted. IPSec Support Support for Internet Protocol Security (IPsec) is provided with Libreswan. Install libreswan Package The libreswan package provides an implementation of IPsec and IKE, which permits the creation of secure tunnels over untrusted networks. The libreswan package can be installed with the following command: $ sudo dnf install libreswan 12 15 3 5 8 APO13.01 DSS01.04 DSS05.02 DSS05.03 DSS05.04 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 SR 1.13 SR 2.6 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.4 A.11.2.6 A.13.1.1 A.13.2.1 A.14.1.3 A.15.1.1 A.15.2.1 A.6.2.1 A.6.2.2 CM-6(a) PR.AC-3 PR.MA-2 PR.PT-4 Req-4.1 SRG-OS-000480-GPOS-00227 SRG-OS-000120-GPOS-00061 Providing the ability for remote users or systems to initiate a secure VPN connection protects information when it is transmitted over a wide area network. if ! rpm -q --quiet "libreswan" ; then dnf install -y "libreswan" fi - name: Ensure libreswan is installed ansible.builtin.package: name: libreswan state: present tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-4.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_libreswan_installed include install_libreswan class install_libreswan { package { 'libreswan': ensure => 'installed', } } package --add=libreswan [[packages]] name = "libreswan" version = "*" package install libreswan dnf install libreswan Verify Any Configured IPSec Tunnel Connections Libreswan provides an implementation of IPsec and IKE, which permits the creation of secure tunnels over untrusted networks. As such, IPsec can be used to circumvent certain network requirements such as filtering. Verify that if any IPsec connection (conn) configured in /etc/ipsec.conf and /etc/ipsec.d exists is an approved organizational connection. Automatic remediation of this control is not available due to the unique requirements of each system. 1 12 13 14 15 16 18 4 6 8 9 APO01.06 APO13.01 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.07 DSS06.02 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.2.3.4 4.3.3.4 4.4.3.3 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 AC-17(a) MA-4(6) CM-6(a) AC-4 SC-8 DE.AE-1 ID.AM-3 PR.AC-5 PR.DS-5 PR.PT-4 SRG-OS-000480-GPOS-00227 IP tunneling mechanisms can be used to bypass network filtering. iptables and ip6tables A host-based firewall called netfilter is included as part of the Linux kernel distributed with the system. It is activated by default. This firewall is controlled by the program iptables, and the entire capability is frequently referred to by this name. An analogous program called ip6tables handles filtering for IPv6. Unlike TCP Wrappers, which depends on the network server program to support and respect the rules written, netfilter filtering occurs at the kernel level, before a program can even process the data from the network packet. As such, any program on the system is affected by the rules written. This section provides basic information about strengthening the iptables and ip6tables configurations included with the system. For more complete information that may allow the construction of a sophisticated ruleset tailored to your environment, please consult the references at the end of this section. Install iptables Package The iptables package can be installed with the following command: $ sudo dnf install iptables CM-6(a) Req-1.4.1 SRG-OS-000480-GPOS-00227 iptables controls the Linux kernel network packet filtering code. iptables allows system operators to set up firewalls and IP masquerading, etc. # Remediation is applicable only in certain platforms if ( ! (systemctl is-active nftables &>/dev/null) && ! (systemctl is-active ufw &>/dev/null) && rpm --quiet -q kernel ); then if ! rpm -q --quiet "iptables" ; then dnf install -y "iptables" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-1.4.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_iptables_installed - name: Ensure iptables is installed ansible.builtin.package: name: iptables state: present when: ( "kernel" in ansible_facts.packages ) tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-1.4.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_iptables_installed include install_iptables class install_iptables { package { 'iptables': ensure => 'installed', } } package --add=iptables [[packages]] name = "iptables" version = "*" package install iptables dnf install iptables Inspect and Activate Default Rules View the currently-enforced iptables rules by running the command: $ sudo iptables -nL --line-numbers The command is analogous for ip6tables. If the firewall does not appear to be active (i.e., no rules appear), activate it and ensure that it starts at boot by issuing the following commands (and analogously for ip6tables): $ sudo service iptables restart The default iptables rules are: Chain INPUT (policy ACCEPT) num target prot opt source destination 1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED 2 ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 3 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 4 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 5 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited Chain FORWARD (policy ACCEPT) num target prot opt source destination 1 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited Chain OUTPUT (policy ACCEPT) num target prot opt source destination The ip6tables default rules are essentially the same. Verify ip6tables Enabled if Using IPv6 The ip6tables service can be enabled with the following command: $ sudo systemctl enable ip6tables.service 1 11 12 13 14 15 16 18 3 4 6 8 9 APO01.06 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R4 CIP-003-8 R5 CIP-004-6 R3 AC-4 CM-7(b) CA-3(5) SC-7(21) CM-6(a) DE.AE-1 ID.AM-3 PR.AC-5 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 The ip6tables service provides the system's host-based firewalling capability for IPv6 and ICMPv6. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'ip6tables.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'ip6tables.service' fi "$SYSTEMCTL_EXEC" enable 'ip6tables.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-4 - NIST-800-53-CA-3(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(21) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_ip6tables_enabled - name: Verify ip6tables Enabled if Using IPv6 - Enable service ip6tables block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Verify ip6tables Enabled if Using IPv6 - Enable Service ip6tables ansible.builtin.systemd: name: ip6tables enabled: true state: started masked: false when: - '"iptables-ipv6" in ansible_facts.packages' tags: - NIST-800-53-AC-4 - NIST-800-53-CA-3(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(21) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_ip6tables_enabled - special_service_block when: '"kernel" in ansible_facts.packages' include enable_ip6tables class enable_ip6tables { service {'ip6tables': enable => true, ensure => 'running', } } [customizations.services] enabled = ["ip6tables"] service enable ip6tables Verify iptables Enabled The iptables service can be enabled with the following command: $ sudo systemctl enable iptables.service 1 11 12 13 14 15 16 18 3 4 6 8 9 APO01.06 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R4 CIP-003-8 R5 CIP-004-6 R3 AC-4 CM-7(b) CA-3(5) SC-7(21) CM-6(a) DE.AE-1 ID.AM-3 PR.AC-5 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 The iptables service provides the system's host-based firewalling capability for IPv4 and ICMP. # Remediation is applicable only in certain platforms if ( rpm --quiet -q iptables && ! (systemctl is-active firewalld &>/dev/null) && rpm --quiet -q kernel ); then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'iptables.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'iptables.service' fi "$SYSTEMCTL_EXEC" enable 'iptables.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-4 - NIST-800-53-CA-3(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(21) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_iptables_enabled - name: Verify iptables Enabled - Enable service iptables block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Verify iptables Enabled - Enable Service iptables ansible.builtin.systemd: name: iptables enabled: true state: started masked: false when: - '"iptables" in ansible_facts.packages' tags: - NIST-800-53-AC-4 - NIST-800-53-CA-3(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(21) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_iptables_enabled - special_service_block when: ( "iptables" in ansible_facts.packages and "kernel" in ansible_facts.packages ) include enable_iptables class enable_iptables { service {'iptables': enable => true, ensure => 'running', } } [customizations.services] enabled = ["iptables"] service enable iptables Set Default ip6tables Policy for Incoming Packets To set the default policy to DROP (instead of ACCEPT) for the built-in INPUT chain which processes incoming packets, add or correct the following line in /etc/sysconfig/ip6tables: :INPUT DROP [0:0] If changes were required, reload the ip6tables rules: $ sudo service ip6tables reload 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CIP-003-8 R4 CIP-003-8 R5 CIP-004-6 R3 AC-4 CM-7(b) CA-3(5) SC-7(21) CM-6(a) PR.IP-1 PR.PT-3 1.4.1 1.4 In ip6tables, the default policy is applied only after all the applicable rules in the table are examined for a match. Setting the default policy to DROP implements proper design for a firewall, i.e. any packets which are not explicitly permitted should not be accepted. # Remediation is applicable only in certain platforms if ( ! ( rpm --quiet -q nftables ) && ! ( rpm --quiet -q ufw ) && rpm --quiet -q iptables ); then sed -i 's/^:INPUT ACCEPT.*/:INPUT DROP [0:0]/g' /etc/sysconfig/ip6tables else >&2 echo 'Remediation is not applicable, nothing was done' fi Strengthen the Default Ruleset The default rules can be strengthened. The system scripts that activate the firewall rules expect them to be defined in the configuration files iptables and ip6tables in the directory /etc/sysconfig. Many of the lines in these files are similar to the command line arguments that would be provided to the programs /sbin/iptables or /sbin/ip6tables - but some are quite different. The following recommendations describe how to strengthen the default ruleset configuration file. An alternative to editing this configuration file is to create a shell script that makes calls to the iptables program to load in rules, and then invokes service iptables save to write those loaded rules to /etc/sysconfig/iptables. The following alterations can be made directly to /etc/sysconfig/iptables and /etc/sysconfig/ip6tables. Instructions apply to both unless otherwise noted. Language and address conventions for regular iptables are used throughout this section; configuration for ip6tables will be either analogous or explicitly covered. The program system-config-securitylevel allows additional services to penetrate the default firewall rules and automatically adjusts /etc/sysconfig/iptables. This program is only useful if the default ruleset meets your security requirements. Otherwise, this program should not be used to make changes to the firewall configuration because it re-writes the saved configuration file. Set Default iptables Policy for Incoming Packets To set the default policy to DROP (instead of ACCEPT) for the built-in INPUT chain which processes incoming packets, add or correct the following line in /etc/sysconfig/iptables: :INPUT DROP [0:0] 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CA-3(5) CM-7(b) SC-7(23) CM-6(a) PR.IP-1 PR.PT-3 In iptables the default policy is applied only after all the applicable rules in the table are examined for a match. Setting the default policy to DROP implements proper design for a firewall, i.e. any packets which are not explicitly permitted should not be accepted. # Remediation is applicable only in certain platforms if rpm --quiet -q iptables && { ( ! ( rpm --quiet -q nftables ) && ! ( rpm --quiet -q ufw ) ); }; then sed -i 's/^:INPUT ACCEPT.*/:INPUT DROP [0:0]/g' /etc/sysconfig/iptables else >&2 echo 'Remediation is not applicable, nothing was done' fi Set Default iptables Policy for Forwarded Packets To set the default policy to DROP (instead of ACCEPT) for the built-in FORWARD chain which processes packets that will be forwarded from one interface to another, add or correct the following line in /etc/sysconfig/iptables: :FORWARD DROP [0:0] 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CA-3(5) CM-7(b) SC-7(23) CM-6(a) PR.IP-1 PR.PT-3 In iptables, the default policy is applied only after all the applicable rules in the table are examined for a match. Setting the default policy to DROP implements proper design for a firewall, i.e. any packets which are not explicitly permitted should not be accepted. # Remediation is applicable only in certain platforms if rpm --quiet -q iptables; then sed -i 's/^:FORWARD ACCEPT.*/:FORWARD DROP [0:0]/g' /etc/sysconfig/iptables else >&2 echo 'Remediation is not applicable, nothing was done' fi IPv6 The system includes support for Internet Protocol version 6. A major and often-mentioned improvement over IPv4 is its enormous increase in the number of available addresses. Another important feature is its support for automatic configuration of many network settings. Disable Support for IPv6 Unless Needed Despite configuration that suggests support for IPv6 has been disabled, link-local IPv6 address auto-configuration occurs even when only an IPv4 address is assigned. The only way to effectively prevent execution of the IPv6 networking stack is to instruct the system not to activate the IPv6 kernel module. Ensure IPv6 is disabled through kernel boot parameter To disable IPv6 protocol support in the Linux kernel, add the argument ipv6.disable=1 to the default GRUB2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain ipv6.disable=1 as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) ipv6.disable=1" Req-1.3.1 Req-1.3.2 Any unnecessary network stacks, including IPv6, should be disabled to reduce the vulnerability to exploitation. # Remediation is applicable only in certain platforms if rpm --quiet -q grub2-common; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "ipv6.disable" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"ipv6.disable=[^\"]*\"(.*]\s*)/\1\"ipv6.disable=1\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"ipv6.disable=1\"]" >> "$KARGS_DIR/10-ipv6_disable.toml" fi else grubby --update-kernel=ALL --args=ipv6.disable=1 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-1.3.1 - PCI-DSS-Req-1.3.2 - grub2_ipv6_disable_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="ipv6.disable=1" when: '"grub2-common" in ansible_facts.packages' tags: - PCI-DSS-Req-1.3.1 - PCI-DSS-Req-1.3.2 - grub2_ipv6_disable_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy [customizations.kernel] append = "ipv6.disable=1" bootloader ipv6.disable=1 Disable IPv6 Networking Support Automatic Loading To prevent the IPv6 kernel module (ipv6) from binding to the IPv6 networking stack, add the following line to /etc/modprobe.d/disabled.conf (or another file in /etc/modprobe.d): options ipv6 disable=1 This permits the IPv6 module to be loaded (and thus satisfy other modules that depend on it), while disabling support for the IPv6 protocol. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Any unnecessary network stacks - including IPv6 - should be disabled, to reduce the vulnerability to exploitation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Prevent the IPv6 kernel module (ipv6) from loading the IPv6 networking stack echo "options ipv6 disable=1" > /etc/modprobe.d/ipv6.conf # Since according to: https://access.redhat.com/solutions/72733 # "ipv6 disable=1" options doesn't always disable the IPv6 networking stack from # loading, instruct also sysctl configuration to disable IPv6 according to: # https://access.redhat.com/solutions/8709#rhel6disable declare -a IPV6_SETTINGS=("net.ipv6.conf.all.disable_ipv6" "net.ipv6.conf.default.disable_ipv6") for setting in "${IPV6_SETTINGS[@]}" do # Set runtime =1 for setting /sbin/sysctl -q -n -w "$setting=1" # If setting is present in /etc/sysctl.conf, change value to "1" # else, add "$setting = 1" to /etc/sysctl.conf if grep -q ^"$setting" /etc/sysctl.conf ; then sed -i "s/^$setting.*/$setting = 1/g" /etc/sysctl.conf else echo "" >> /etc/sysctl.conf echo "# Set $setting = 1 per security requirements" >> /etc/sysctl.conf echo "$setting = 1" >> /etc/sysctl.conf fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_ipv6_option_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Disable IPv6 Networking kernel module ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/ipv6.conf regexp: ^options\s+ipv6\s+disable=\d line: options ipv6 disable=1 when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_ipv6_option_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure disable_ipv6 (all and default) is set to 1 ansible.posix.sysctl: name: '{{ item }}' value: '1' state: present reload: true with_items: - net.ipv6.conf.all.disable_ipv6 - net.ipv6.conf.default.disable_ipv6 when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_ipv6_option_disabled - low_complexity - medium_disruption - medium_severity - reboot_required Disable Interface Usage of IPv6 To disable interface usage of IPv6, add or correct the following lines in /etc/sysconfig/network: NETWORKING_IPV6=no IPV6INIT=no Disable Support for RPC IPv6 RPC services for NFSv4 try to load transport modules for udp6 and tcp6 by default, even if IPv6 has been disabled in /etc/modprobe.d. To prevent RPC services such as rpc.mountd from attempting to start IPv6 network listeners, remove or comment out the following two lines in /etc/netconfig: udp6 tpi_clts v inet6 udp - - tcp6 tpi_cots_ord v inet6 tcp - - 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Disable IPv6 Addressing on All IPv6 Interfaces To disable support for (ipv6) addressing on all interface add the following line to /etc/sysctl.d/ipv6.conf (or another file in /etc/sysctl.d): net.ipv6.conf.all.disable_ipv6 = 1 This disables IPv6 on all network interfaces as other services and system functionality require the IPv6 stack loaded to work. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Any unnecessary network stacks - including IPv6 - should be disabled, to reduce the vulnerability to exploitation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.disable_ipv6 from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.disable_ipv6.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.disable_ipv6" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv6.conf.all.disable_ipv6 # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.disable_ipv6="1" fi # # If net.ipv6.conf.all.disable_ipv6 present in /etc/sysctl.conf, change value to "1" # else, add "net.ipv6.conf.all.disable_ipv6 = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.disable_ipv6") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.disable_ipv6\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.disable_ipv6\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_disable_ipv6 - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.disable_ipv6.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_disable_ipv6 - name: Comment out any occurrences of net.ipv6.conf.all.disable_ipv6 from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.disable_ipv6 replace: '#net.ipv6.conf.all.disable_ipv6' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_disable_ipv6 - name: Ensure sysctl net.ipv6.conf.all.disable_ipv6 is set to 1 ansible.posix.sysctl: name: net.ipv6.conf.all.disable_ipv6 value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_disable_ipv6 Disable IPv6 Addressing on IPv6 Interfaces by Default To disable support for (ipv6) addressing on interfaces by default add the following line to /etc/sysctl.d/ipv6.conf (or another file in /etc/sysctl.d): net.ipv6.conf.default.disable_ipv6 = 1 This disables IPv6 on network interfaces by default as other services and system functionality require the IPv6 stack loaded to work. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Any unnecessary network stacks - including IPv6 - should be disabled, to reduce the vulnerability to exploitation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.disable_ipv6 from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.disable_ipv6.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.disable_ipv6" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv6.conf.default.disable_ipv6 # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.disable_ipv6="1" fi # # If net.ipv6.conf.default.disable_ipv6 present in /etc/sysctl.conf, change value to "1" # else, add "net.ipv6.conf.default.disable_ipv6 = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.disable_ipv6") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.disable_ipv6\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.disable_ipv6\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_disable_ipv6 - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.disable_ipv6.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_disable_ipv6 - name: Comment out any occurrences of net.ipv6.conf.default.disable_ipv6 from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.disable_ipv6 replace: '#net.ipv6.conf.default.disable_ipv6' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_disable_ipv6 - name: Ensure sysctl net.ipv6.conf.default.disable_ipv6 is set to 1 ansible.posix.sysctl: name: net.ipv6.conf.default.disable_ipv6 value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_disable_ipv6 Configure IPv6 Settings if Necessary A major feature of IPv6 is the extent to which systems implementing it can automatically configure their networking devices using information from the network. From a security perspective, manually configuring important configuration information is preferable to accepting it from the network in an unauthenticated fashion. net.ipv6.conf.all.accept_ra_defrtr Accept default router in router advertisements? 0 0 1 net.ipv6.conf.all.accept_ra_pinfo Accept prefix information in router advertisements? 0 0 1 net.ipv6.conf.all.accept_ra_rtr_pref Accept router preference in router advertisements? 0 0 1 net.ipv6.conf.all.accept_ra Accept all router advertisements? 0 0 1 net.ipv6.conf.all.accept_redirects Toggle ICMP Redirect Acceptance 0 0 1 net.ipv6.conf.all.accept_source_route Trackers could be using source-routed packets to generate traffic that seems to be intra-net, but actually was created outside and has been redirected. 0 0 1 net.ipv6.conf.all.autoconf Enable auto configuration on IPv6 interfaces 0 0 1 net.ipv6.conf.all.forwarding Toggle IPv6 Forwarding 0 0 1 net.ipv6.conf.all.max_addresses Maximum number of autoconfigured IPv6 addresses 1 net.ipv6.conf.all.router_solicitations Accept all router solicitations? 0 0 1 net.ipv6.conf.default.accept_ra_defrtr Accept default router in router advertisements? 0 0 1 net.ipv6.conf.default.accept_ra_pinfo Accept prefix information in router advertisements? 0 0 1 net.ipv6.conf.default.accept_ra_rtr_pref Accept router preference in router advertisements? 0 0 1 net.ipv6.conf.default.accept_ra Accept default router advertisements by default? 0 0 1 net.ipv6.conf.default.accept_redirects Toggle ICMP Redirect Acceptance By Default 0 0 1 net.ipv6.conf.default.accept_source_route Trackers could be using source-routed packets to generate traffic that seems to be intra-net, but actually was created outside and has been redirected. 0 0 1 net.ipv6.conf.default.autoconf Enable auto configuration on IPv6 interfaces 0 0 1 net.ipv6.conf.default.max_addresses Maximum number of autoconfigured IPv6 addresses 1 net.ipv6.conf.default.router_solicitations Accept all router solicitations by default? 0 0 1 Manually Assign IPv6 Router Address Edit the file /etc/sysconfig/network-scripts/ifcfg-interface , and add or correct the following line (substituting your gateway IP as appropriate): IPV6_DEFAULTGW=2001:0DB8::0001 Router addresses should be manually set and not accepted via any auto-configuration or router advertisement. Use Privacy Extensions for Address To introduce randomness into the automatic generation of IPv6 addresses, add or correct the following line in /etc/sysconfig/network-scripts/ifcfg-interface : IPV6_PRIVACY=rfc3041 Automatically-generated IPv6 addresses are based on the underlying hardware (e.g. Ethernet) address, and so it becomes possible to track a piece of hardware over its lifetime using its traffic. If it is important for a system's IP address to not trivially reveal its hardware address, this setting should be applied. 3.1.20 Manually Assign Global IPv6 Address To manually assign an IP address for an interface, edit the file /etc/sysconfig/network-scripts/ifcfg-interface . Add or correct the following line (substituting the correct IPv6 address): IPV6ADDR=2001:0DB8::ABCD/64 Manually assigning an IP address is preferable to accepting one from routers or from the network otherwise. The example address here is an IPv6 address reserved for documentation purposes, as defined by RFC3849. 1315 1319 Configure Accepting Router Advertisements on All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.accept_ra kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.accept_ra=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.accept_ra = 0 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 3.3.2.7 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.accept_ra from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_ra.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.accept_ra" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_accept_ra_value='' # # Set runtime for net.ipv6.conf.all.accept_ra # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_ra="$sysctl_net_ipv6_conf_all_accept_ra_value" fi # # If net.ipv6.conf.all.accept_ra present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.accept_ra = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_ra") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_ra_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_ra\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_ra\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_ra - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.accept_ra.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_ra - name: Comment out any occurrences of net.ipv6.conf.all.accept_ra from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.accept_ra replace: '#net.ipv6.conf.all.accept_ra' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_ra - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_ra_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_accept_ra_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.accept_ra is set ansible.posix.sysctl: name: net.ipv6.conf.all.accept_ra value: '{{ sysctl_net_ipv6_conf_all_accept_ra_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_ra --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv6.conf.all.accept_ra%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_all_accept_ra.conf overwrite: true Configure Accepting Default Router in Router Advertisements on All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.accept_ra_defrtr kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.accept_ra_defrtr=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.accept_ra_defrtr = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.accept_ra_defrtr from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_ra_defrtr.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.accept_ra_defrtr" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_accept_ra_defrtr_value='' # # Set runtime for net.ipv6.conf.all.accept_ra_defrtr # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_ra_defrtr="$sysctl_net_ipv6_conf_all_accept_ra_defrtr_value" fi # # If net.ipv6.conf.all.accept_ra_defrtr present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.accept_ra_defrtr = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_ra_defrtr") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_ra_defrtr_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_ra_defrtr\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_ra_defrtr\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_defrtr - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.accept_ra_defrtr.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_defrtr - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.all.accept_ra_defrtr from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.accept_ra_defrtr replace: '#net.ipv6.conf.all.accept_ra_defrtr' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_defrtr - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_ra_defrtr_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_accept_ra_defrtr_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.accept_ra_defrtr is set ansible.posix.sysctl: name: net.ipv6.conf.all.accept_ra_defrtr value: '{{ sysctl_net_ipv6_conf_all_accept_ra_defrtr_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_defrtr - unknown_severity Configure Accepting Prefix Information in Router Advertisements on All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.accept_ra_pinfo kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.accept_ra_pinfo=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.accept_ra_pinfo = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.accept_ra_pinfo from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_ra_pinfo.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.accept_ra_pinfo" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_accept_ra_pinfo_value='' # # Set runtime for net.ipv6.conf.all.accept_ra_pinfo # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_ra_pinfo="$sysctl_net_ipv6_conf_all_accept_ra_pinfo_value" fi # # If net.ipv6.conf.all.accept_ra_pinfo present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.accept_ra_pinfo = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_ra_pinfo") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_ra_pinfo_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_ra_pinfo\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_ra_pinfo\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_pinfo - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.accept_ra_pinfo.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_pinfo - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.all.accept_ra_pinfo from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.accept_ra_pinfo replace: '#net.ipv6.conf.all.accept_ra_pinfo' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_pinfo - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_ra_pinfo_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_accept_ra_pinfo_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.accept_ra_pinfo is set ansible.posix.sysctl: name: net.ipv6.conf.all.accept_ra_pinfo value: '{{ sysctl_net_ipv6_conf_all_accept_ra_pinfo_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_pinfo - unknown_severity Configure Accepting Router Preference in Router Advertisements on All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.accept_ra_rtr_pref kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.accept_ra_rtr_pref=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.accept_ra_rtr_pref = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.accept_ra_rtr_pref from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_ra_rtr_pref.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.accept_ra_rtr_pref" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_accept_ra_rtr_pref_value='' # # Set runtime for net.ipv6.conf.all.accept_ra_rtr_pref # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_ra_rtr_pref="$sysctl_net_ipv6_conf_all_accept_ra_rtr_pref_value" fi # # If net.ipv6.conf.all.accept_ra_rtr_pref present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.accept_ra_rtr_pref = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_ra_rtr_pref") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_ra_rtr_pref_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_ra_rtr_pref\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_ra_rtr_pref\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_rtr_pref - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.accept_ra_rtr_pref.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_rtr_pref - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.all.accept_ra_rtr_pref from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.accept_ra_rtr_pref replace: '#net.ipv6.conf.all.accept_ra_rtr_pref' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_rtr_pref - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_ra_rtr_pref_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_accept_ra_rtr_pref_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.accept_ra_rtr_pref is set ansible.posix.sysctl: name: net.ipv6.conf.all.accept_ra_rtr_pref value: '{{ sysctl_net_ipv6_conf_all_accept_ra_rtr_pref_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_accept_ra_rtr_pref - unknown_severity Disable Accepting ICMP Redirects for All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.accept_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.accept_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.accept_redirects = 0 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) CM-6(b) CM-6.1(iv) PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 R13 3.3.2.3 An illicit ICMP redirect message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.accept_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.accept_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_accept_redirects_value='' # # Set runtime for net.ipv6.conf.all.accept_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_redirects="$sysctl_net_ipv6_conf_all_accept_redirects_value" fi # # If net.ipv6.conf.all.accept_redirects present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.accept_redirects = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_redirects_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.accept_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_redirects - name: Comment out any occurrences of net.ipv6.conf.all.accept_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.accept_redirects replace: '#net.ipv6.conf.all.accept_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_redirects - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_redirects_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_accept_redirects_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.accept_redirects is set ansible.posix.sysctl: name: net.ipv6.conf.all.accept_redirects value: '{{ sysctl_net_ipv6_conf_all_accept_redirects_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv6.conf.all.accept_redirects%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_all_accept_redirects.conf overwrite: true Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.accept_source_route kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.accept_source_route=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.accept_source_route = 0 1 12 13 14 15 16 18 4 6 8 9 APO01.06 APO13.01 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.07 DSS06.02 3.1.20 4.2.3.4 4.3.3.4 4.4.3.3 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) DE.AE-1 ID.AM-3 PR.AC-5 PR.DS-5 PR.PT-4 SRG-OS-000480-GPOS-00227 R13 3.3.2.5 Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv6 forwarding is enabled and the system is functioning as a router. Accepting source-routed packets in the IPv6 protocol has few legitimate uses. It should be disabled unless it is absolutely required. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.accept_source_route from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_source_route.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.accept_source_route" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_accept_source_route_value='' # # Set runtime for net.ipv6.conf.all.accept_source_route # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_source_route="$sysctl_net_ipv6_conf_all_accept_source_route_value" fi # # If net.ipv6.conf.all.accept_source_route present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.accept_source_route = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_source_route") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_source_route_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_source_route\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_source_route - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.accept_source_route.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_source_route - name: Comment out any occurrences of net.ipv6.conf.all.accept_source_route from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.accept_source_route replace: '#net.ipv6.conf.all.accept_source_route' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_source_route - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_source_route_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_accept_source_route_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.accept_source_route is set ansible.posix.sysctl: name: net.ipv6.conf.all.accept_source_route value: '{{ sysctl_net_ipv6_conf_all_accept_source_route_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_accept_source_route --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv6.conf.all.accept_source_route%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_all_accept_source_route.conf overwrite: true Configure Auto Configuration on All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.autoconf kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.autoconf=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.autoconf = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.autoconf from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.autoconf.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.autoconf" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_autoconf_value='' # # Set runtime for net.ipv6.conf.all.autoconf # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.autoconf="$sysctl_net_ipv6_conf_all_autoconf_value" fi # # If net.ipv6.conf.all.autoconf present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.autoconf = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.autoconf") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_autoconf_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.autoconf\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.autoconf\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_autoconf - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.autoconf.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_autoconf - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.all.autoconf from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.autoconf replace: '#net.ipv6.conf.all.autoconf' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_autoconf - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_all_autoconf_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_autoconf_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.autoconf is set ansible.posix.sysctl: name: net.ipv6.conf.all.autoconf value: '{{ sysctl_net_ipv6_conf_all_autoconf_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_autoconf - unknown_severity Disable Kernel Parameter for IPv6 Forwarding To set the runtime status of the net.ipv6.conf.all.forwarding kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.forwarding=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.forwarding = 0 1 11 12 13 14 15 16 2 3 7 8 9 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS05.02 DSS05.05 DSS05.07 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) CM-6(b) CM-6.1(iv) DE.CM-1 PR.DS-4 PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 3.3.2.1 IP forwarding permits the kernel to forward packets from one network interface to another. The ability to forward packets between two networks is only appropriate for systems acting as routers. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.forwarding from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.forwarding.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.forwarding" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_forwarding_value='' # # Set runtime for net.ipv6.conf.all.forwarding # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.forwarding="$sysctl_net_ipv6_conf_all_forwarding_value" fi # # If net.ipv6.conf.all.forwarding present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.forwarding = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.forwarding") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_forwarding_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.forwarding\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.forwarding\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_forwarding - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.forwarding.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_forwarding - name: Comment out any occurrences of net.ipv6.conf.all.forwarding from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.forwarding replace: '#net.ipv6.conf.all.forwarding' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_forwarding - name: XCCDF Value sysctl_net_ipv6_conf_all_forwarding_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_forwarding_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.forwarding is set ansible.posix.sysctl: name: net.ipv6.conf.all.forwarding value: '{{ sysctl_net_ipv6_conf_all_forwarding_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_all_forwarding Configure Maximum Number of Autoconfigured Addresses on All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.max_addresses kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.max_addresses=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.max_addresses = 1 R13 The number of global unicast IPv6 addresses for each interface should be limited exactly to the number of statically configured addresses. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.max_addresses from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.max_addresses.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.max_addresses" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_max_addresses_value='' # # Set runtime for net.ipv6.conf.all.max_addresses # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.max_addresses="$sysctl_net_ipv6_conf_all_max_addresses_value" fi # # If net.ipv6.conf.all.max_addresses present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.max_addresses = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.max_addresses") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_max_addresses_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.max_addresses\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.max_addresses\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_max_addresses - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.max_addresses.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_max_addresses - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.all.max_addresses from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.max_addresses replace: '#net.ipv6.conf.all.max_addresses' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_max_addresses - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_all_max_addresses_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_max_addresses_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.max_addresses is set ansible.posix.sysctl: name: net.ipv6.conf.all.max_addresses value: '{{ sysctl_net_ipv6_conf_all_max_addresses_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_max_addresses - unknown_severity Configure Denying Router Solicitations on All IPv6 Interfaces To set the runtime status of the net.ipv6.conf.all.router_solicitations kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.all.router_solicitations=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.all.router_solicitations = 0 R13 To prevent discovery of the system by other systems, router solicitation requests should be denied. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.all.router_solicitations from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.router_solicitations.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.all.router_solicitations" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_all_router_solicitations_value='' # # Set runtime for net.ipv6.conf.all.router_solicitations # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.all.router_solicitations="$sysctl_net_ipv6_conf_all_router_solicitations_value" fi # # If net.ipv6.conf.all.router_solicitations present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.all.router_solicitations = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.router_solicitations") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_router_solicitations_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.router_solicitations\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.router_solicitations\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_router_solicitations - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.all.router_solicitations.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_router_solicitations - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.all.router_solicitations from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.all.router_solicitations replace: '#net.ipv6.conf.all.router_solicitations' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_router_solicitations - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_all_router_solicitations_value # promote to variable set_fact: sysctl_net_ipv6_conf_all_router_solicitations_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.all.router_solicitations is set ansible.posix.sysctl: name: net.ipv6.conf.all.router_solicitations value: '{{ sysctl_net_ipv6_conf_all_router_solicitations_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_all_router_solicitations - unknown_severity Disable Accepting Router Advertisements on all IPv6 Interfaces by Default To set the runtime status of the net.ipv6.conf.default.accept_ra kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.accept_ra=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.accept_ra = 0 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 3.3.2.8 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.accept_ra from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_ra.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.accept_ra" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_accept_ra_value='' # # Set runtime for net.ipv6.conf.default.accept_ra # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_ra="$sysctl_net_ipv6_conf_default_accept_ra_value" fi # # If net.ipv6.conf.default.accept_ra present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.accept_ra = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_ra") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_ra_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_ra\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_ra\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_ra - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.accept_ra.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_ra - name: Comment out any occurrences of net.ipv6.conf.default.accept_ra from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.accept_ra replace: '#net.ipv6.conf.default.accept_ra' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_ra - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_ra_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_accept_ra_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.accept_ra is set ansible.posix.sysctl: name: net.ipv6.conf.default.accept_ra value: '{{ sysctl_net_ipv6_conf_default_accept_ra_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_ra --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv6.conf.default.accept_ra%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_default_accept_ra.conf overwrite: true Configure Accepting Default Router in Router Advertisements on All IPv6 Interfaces By Default To set the runtime status of the net.ipv6.conf.default.accept_ra_defrtr kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.accept_ra_defrtr=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.accept_ra_defrtr = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.accept_ra_defrtr from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_ra_defrtr.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.accept_ra_defrtr" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_accept_ra_defrtr_value='' # # Set runtime for net.ipv6.conf.default.accept_ra_defrtr # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_ra_defrtr="$sysctl_net_ipv6_conf_default_accept_ra_defrtr_value" fi # # If net.ipv6.conf.default.accept_ra_defrtr present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.accept_ra_defrtr = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_ra_defrtr") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_ra_defrtr_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_ra_defrtr\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_ra_defrtr\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_defrtr - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.accept_ra_defrtr.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_defrtr - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.default.accept_ra_defrtr from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.accept_ra_defrtr replace: '#net.ipv6.conf.default.accept_ra_defrtr' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_defrtr - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_ra_defrtr_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_accept_ra_defrtr_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.accept_ra_defrtr is set ansible.posix.sysctl: name: net.ipv6.conf.default.accept_ra_defrtr value: '{{ sysctl_net_ipv6_conf_default_accept_ra_defrtr_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_defrtr - unknown_severity Configure Accepting Prefix Information in Router Advertisements on All IPv6 Interfaces By Default To set the runtime status of the net.ipv6.conf.default.accept_ra_pinfo kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.accept_ra_pinfo=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.accept_ra_pinfo = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.accept_ra_pinfo from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_ra_pinfo.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.accept_ra_pinfo" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_accept_ra_pinfo_value='' # # Set runtime for net.ipv6.conf.default.accept_ra_pinfo # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_ra_pinfo="$sysctl_net_ipv6_conf_default_accept_ra_pinfo_value" fi # # If net.ipv6.conf.default.accept_ra_pinfo present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.accept_ra_pinfo = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_ra_pinfo") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_ra_pinfo_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_ra_pinfo\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_ra_pinfo\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_pinfo - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.accept_ra_pinfo.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_pinfo - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.default.accept_ra_pinfo from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.accept_ra_pinfo replace: '#net.ipv6.conf.default.accept_ra_pinfo' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_pinfo - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_ra_pinfo_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_accept_ra_pinfo_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.accept_ra_pinfo is set ansible.posix.sysctl: name: net.ipv6.conf.default.accept_ra_pinfo value: '{{ sysctl_net_ipv6_conf_default_accept_ra_pinfo_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_pinfo - unknown_severity Configure Accepting Router Preference in Router Advertisements on All IPv6 Interfaces By Default To set the runtime status of the net.ipv6.conf.default.accept_ra_rtr_pref kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.accept_ra_rtr_pref=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.accept_ra_rtr_pref = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.accept_ra_rtr_pref from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_ra_rtr_pref.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.accept_ra_rtr_pref" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_accept_ra_rtr_pref_value='' # # Set runtime for net.ipv6.conf.default.accept_ra_rtr_pref # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_ra_rtr_pref="$sysctl_net_ipv6_conf_default_accept_ra_rtr_pref_value" fi # # If net.ipv6.conf.default.accept_ra_rtr_pref present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.accept_ra_rtr_pref = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_ra_rtr_pref") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_ra_rtr_pref_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_ra_rtr_pref\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_ra_rtr_pref\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_rtr_pref - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.accept_ra_rtr_pref.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_rtr_pref - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.default.accept_ra_rtr_pref from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.accept_ra_rtr_pref replace: '#net.ipv6.conf.default.accept_ra_rtr_pref' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_rtr_pref - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_ra_rtr_pref_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_accept_ra_rtr_pref_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.accept_ra_rtr_pref is set ansible.posix.sysctl: name: net.ipv6.conf.default.accept_ra_rtr_pref value: '{{ sysctl_net_ipv6_conf_default_accept_ra_rtr_pref_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_accept_ra_rtr_pref - unknown_severity Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces To set the runtime status of the net.ipv6.conf.default.accept_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.accept_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.accept_redirects = 0 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 R13 3.3.2.4 An illicit ICMP redirect message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.accept_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.accept_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_accept_redirects_value='' # # Set runtime for net.ipv6.conf.default.accept_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_redirects="$sysctl_net_ipv6_conf_default_accept_redirects_value" fi # # If net.ipv6.conf.default.accept_redirects present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.accept_redirects = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_redirects_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.accept_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_redirects - name: Comment out any occurrences of net.ipv6.conf.default.accept_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.accept_redirects replace: '#net.ipv6.conf.default.accept_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_redirects - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_redirects_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_accept_redirects_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.accept_redirects is set ansible.posix.sysctl: name: net.ipv6.conf.default.accept_redirects value: '{{ sysctl_net_ipv6_conf_default_accept_redirects_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv6.conf.default.accept_redirects%20%3D%200%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_default_accept_redirects.conf overwrite: true Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces by Default To set the runtime status of the net.ipv6.conf.default.accept_source_route kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.accept_source_route=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.accept_source_route = 0 1 12 13 14 15 16 18 4 6 8 9 APO01.06 APO13.01 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.07 DSS06.02 3.1.20 4.2.3.4 4.3.3.4 4.4.3.3 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) CM-6(b) CM-6.1(iv) DE.AE-1 ID.AM-3 PR.AC-5 PR.DS-5 PR.PT-4 Req-1.4.3 SRG-OS-000480-GPOS-00227 R13 3.3.2.6 1.4.2 1.4 Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv6 forwarding is enabled and the system is functioning as a router. Accepting source-routed packets in the IPv6 protocol has few legitimate uses. It should be disabled unless it is absolutely required. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.accept_source_route from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_source_route.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.accept_source_route" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_accept_source_route_value='' # # Set runtime for net.ipv6.conf.default.accept_source_route # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_source_route="$sysctl_net_ipv6_conf_default_accept_source_route_value" fi # # If net.ipv6.conf.default.accept_source_route present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.accept_source_route = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_source_route") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_source_route_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_source_route\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_source_route - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.accept_source_route.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_source_route - name: Comment out any occurrences of net.ipv6.conf.default.accept_source_route from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.accept_source_route replace: '#net.ipv6.conf.default.accept_source_route' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_source_route - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_source_route_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_accept_source_route_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.accept_source_route is set ansible.posix.sysctl: name: net.ipv6.conf.default.accept_source_route value: '{{ sysctl_net_ipv6_conf_default_accept_source_route_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(b) - NIST-800-53-CM-6.1(iv) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv6_conf_default_accept_source_route --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv6.conf.default.accept_source_route%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_default_accept_source_route.conf overwrite: true Configure Auto Configuration on All IPv6 Interfaces By Default To set the runtime status of the net.ipv6.conf.default.autoconf kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.autoconf=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.autoconf = 0 R13 An illicit router advertisement message could result in a man-in-the-middle attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.autoconf from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.autoconf.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.autoconf" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_autoconf_value='' # # Set runtime for net.ipv6.conf.default.autoconf # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.autoconf="$sysctl_net_ipv6_conf_default_autoconf_value" fi # # If net.ipv6.conf.default.autoconf present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.autoconf = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.autoconf") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_autoconf_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.autoconf\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.autoconf\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_autoconf - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.autoconf.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_autoconf - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.default.autoconf from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.autoconf replace: '#net.ipv6.conf.default.autoconf' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_autoconf - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_default_autoconf_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_autoconf_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.autoconf is set ansible.posix.sysctl: name: net.ipv6.conf.default.autoconf value: '{{ sysctl_net_ipv6_conf_default_autoconf_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_autoconf - unknown_severity Configure Maximum Number of Autoconfigured Addresses on All IPv6 Interfaces By Default To set the runtime status of the net.ipv6.conf.default.max_addresses kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.max_addresses=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.max_addresses = 1 R13 The number of global unicast IPv6 addresses for each interface should be limited exactly to the number of statically configured addresses. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.max_addresses from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.max_addresses.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.max_addresses" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_max_addresses_value='' # # Set runtime for net.ipv6.conf.default.max_addresses # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.max_addresses="$sysctl_net_ipv6_conf_default_max_addresses_value" fi # # If net.ipv6.conf.default.max_addresses present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.max_addresses = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.max_addresses") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_max_addresses_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.max_addresses\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.max_addresses\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_max_addresses - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.max_addresses.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_max_addresses - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.default.max_addresses from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.max_addresses replace: '#net.ipv6.conf.default.max_addresses' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_max_addresses - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_default_max_addresses_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_max_addresses_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.max_addresses is set ansible.posix.sysctl: name: net.ipv6.conf.default.max_addresses value: '{{ sysctl_net_ipv6_conf_default_max_addresses_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_max_addresses - unknown_severity Configure Denying Router Solicitations on All IPv6 Interfaces By Default To set the runtime status of the net.ipv6.conf.default.router_solicitations kernel parameter, run the following command: $ sudo sysctl -w net.ipv6.conf.default.router_solicitations=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv6.conf.default.router_solicitations = 0 R13 To prevent discovery of the system by other systems, router solicitation requests should be denied. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv6.conf.default.router_solicitations from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.router_solicitations.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv6.conf.default.router_solicitations" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv6_conf_default_router_solicitations_value='' # # Set runtime for net.ipv6.conf.default.router_solicitations # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv6.conf.default.router_solicitations="$sysctl_net_ipv6_conf_default_router_solicitations_value" fi # # If net.ipv6.conf.default.router_solicitations present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv6.conf.default.router_solicitations = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.router_solicitations") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_router_solicitations_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.router_solicitations\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.router_solicitations\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_router_solicitations - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv6.conf.default.router_solicitations.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_router_solicitations - unknown_severity - name: Comment out any occurrences of net.ipv6.conf.default.router_solicitations from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv6.conf.default.router_solicitations replace: '#net.ipv6.conf.default.router_solicitations' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_router_solicitations - unknown_severity - name: XCCDF Value sysctl_net_ipv6_conf_default_router_solicitations_value # promote to variable set_fact: sysctl_net_ipv6_conf_default_router_solicitations_value: !!str tags: - always - name: Ensure sysctl net.ipv6.conf.default.router_solicitations is set ansible.posix.sysctl: name: net.ipv6.conf.default.router_solicitations value: '{{ sysctl_net_ipv6_conf_default_router_solicitations_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv6_conf_default_router_solicitations - unknown_severity Kernel Parameters Which Affect Networking The sysctl utility is used to set parameters which affect the operation of the Linux kernel. Kernel parameters which affect networking and have security implications are described here. Network Related Kernel Runtime Parameters for Hosts and Routers Certain kernel parameters should be set for systems which are acting as either hosts or routers to improve the system's ability defend against certain types of IPv4 protocol attacks. net.ipv4.conf.all.accept_redirects Disable ICMP Redirect Acceptance 0 0 1 net.ipv4.conf.all.accept_source_route Trackers could be using source-routed packets to generate traffic that seems to be intra-net, but actually was created outside and has been redirected. 0 0 1 net.ipv4.conf.default.arp_filter Controls whether the ARP filter is enabled or not. 1 - Allows you to have multiple network interfaces on the same subnet, and have the ARPs for each interface be answered based on whether or not the kernel would route a packet from the ARP’d IP out that interface. In other words it allows control of which cards (usually 1) will respond to an ARP request. 0 - (default) The kernel can respond to arp requests with addresses from other interfaces. This may seem wrong but it usually makes sense, because it increases the chance of successful communication. IP addresses are owned by the complete host on Linux, not by particular interfaces. 0 0 1 net.ipv4.conf.default.arp_ignore Control the response modes for ARP queries that resolve local target IP addresses: 0 - (default): reply for any local target IP address, configured on any interface 1 - reply only if the target IP address is local address configured on the incoming interface 2 - reply only if the target IP address is local address configured on the incoming interface and both with the sender’s IP address are part from same subnet on this interface 3 - do not reply for local addresses configured with scope host, only resolutions for global and link addresses are replied 4-7 - reserved 8 - do not reply for all local addresses 0 0 1 2 3 8 net.ipv4.conf.all.log_martians Disable so you don't Log Spoofed Packets, Source Routed Packets, Redirect Packets 1 0 1 net.ipv4.conf.all.rp_filter Enable to enforce sanity checking, also called ingress filtering or egress filtering. The point is to drop a packet if the source and destination IP addresses in the IP header do not make sense when considered in light of the physical interface on which it arrived. 1 1 2 net.ipv4.conf.all.secure_redirects Enable to prevent hijacking of routing path by only allowing redirects from gateways known in routing table. Disable to refuse acceptance of secure ICMP redirected packets on all interfaces. 0 0 1 net.ipv4.conf.all.shared_media Controls whether the system can send (router) or accept (host) RFC1620 shared media redirects. shared_media for the interface will be enabled if at least one of conf/{all,interface}/shared_media is set to TRUE, it will be disabled otherwise. 0 0 1 net.ipv4.conf.default.accept_redirects Disable ICMP Redirect Acceptance? 0 0 1 net.ipv4.conf.default.accept_source_route Disable IP source routing? 0 0 1 net.ipv4.conf.default.log_martians Disable so you don't Log Spoofed Packets, Source Routed Packets, Redirect Packets 1 0 1 net.ipv4.conf.default.rp_filter Enables source route verification 1 0 1 net.ipv4.conf.default.secure_redirects Enable to prevent hijacking of routing path by only allowing redirects from gateways known in routing table. Disable to refuse acceptance of secure ICMP redirected packages by default. 0 0 1 net.ipv4.conf.default.shared_media Controls whether the system can send(router) or accept(host) RFC1620 shared media redirects. shared_media for the interface will be enabled if at least one of conf/{all,interface}/shared_media is set to TRUE, it will be disabled otherwise. 0 0 1 net.ipv4.icmp_echo_ignore_broadcasts Ignore all ICMP ECHO and TIMESTAMP requests sent to it via broadcast/multicast 1 0 1 net.ipv4.icmp_ignore_bogus_error_responses Enable to prevent unnecessary logging 1 0 1 net.ipv4.tcp_syncookies Enable to turn on TCP SYN Cookie Protection 1 0 1 Disable Accepting Packets Routed Between Local Interfaces To set the runtime status of the net.ipv4.conf.all.accept_local kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.accept_local=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.accept_local = 0 R12 Configure net.ipv4.conf.all.accept_local=0 to consider as invalid the packets received from outside whose source is the 127.0.0.0/8 address block. In combination with suitable routing, this can be used to direct packets between two local interfaces over the wire and have them accepted properly. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.accept_local from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.accept_local.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.accept_local" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv4.conf.all.accept_local # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.accept_local="0" fi # # If net.ipv4.conf.all.accept_local present in /etc/sysctl.conf, change value to "0" # else, add "net.ipv4.conf.all.accept_local = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.accept_local") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.accept_local\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.accept_local\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_local - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.accept_local.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_local - name: Comment out any occurrences of net.ipv4.conf.all.accept_local from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.accept_local replace: '#net.ipv4.conf.all.accept_local' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_local - name: Ensure sysctl net.ipv4.conf.all.accept_local is set to 0 ansible.posix.sysctl: name: net.ipv4.conf.all.accept_local value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_local Disable Accepting ICMP Redirects for All IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.accept_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.accept_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.accept_redirects = 0 1 11 12 13 14 15 16 2 3 7 8 9 5.10.1.1 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS05.02 DSS05.05 DSS05.07 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) SC-7(a) DE.CM-1 PR.DS-4 PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 R12 3.3.1.8 ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages modify the host's route table and are unauthenticated. An illicit ICMP redirect message could result in a man-in-the-middle attack. This feature of the IPv4 protocol has few legitimate uses. It should be disabled unless absolutely required." # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.accept_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.accept_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.accept_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_accept_redirects_value='' # # Set runtime for net.ipv4.conf.all.accept_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.accept_redirects="$sysctl_net_ipv4_conf_all_accept_redirects_value" fi # # If net.ipv4.conf.all.accept_redirects present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.accept_redirects = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.accept_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_accept_redirects_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.accept_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.accept_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_redirects - name: Comment out any occurrences of net.ipv4.conf.all.accept_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.accept_redirects replace: '#net.ipv4.conf.all.accept_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_redirects - name: XCCDF Value sysctl_net_ipv4_conf_all_accept_redirects_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_accept_redirects_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.accept_redirects is set ansible.posix.sysctl: name: net.ipv4.conf.all.accept_redirects value: '{{ sysctl_net_ipv4_conf_all_accept_redirects_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.all.accept_redirects%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_accept_redirects.conf overwrite: true Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.accept_source_route kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.accept_source_route=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.accept_source_route = 0 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 CM-6(a) SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 R12 3.3.1.14 Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv4 forwarding is enabled and the system is functioning as a router. Accepting source-routed packets in the IPv4 protocol has few legitimate uses. It should be disabled unless it is absolutely required. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.accept_source_route from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.accept_source_route.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.accept_source_route" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_accept_source_route_value='' # # Set runtime for net.ipv4.conf.all.accept_source_route # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.accept_source_route="$sysctl_net_ipv4_conf_all_accept_source_route_value" fi # # If net.ipv4.conf.all.accept_source_route present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.accept_source_route = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.accept_source_route") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_accept_source_route_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.accept_source_route\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_source_route - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.accept_source_route.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_source_route - name: Comment out any occurrences of net.ipv4.conf.all.accept_source_route from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.accept_source_route replace: '#net.ipv4.conf.all.accept_source_route' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_source_route - name: XCCDF Value sysctl_net_ipv4_conf_all_accept_source_route_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_accept_source_route_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.accept_source_route is set ansible.posix.sysctl: name: net.ipv4.conf.all.accept_source_route value: '{{ sysctl_net_ipv4_conf_all_accept_source_route_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_accept_source_route --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.all.accept_source_route%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_accept_source_route.conf overwrite: true Configure ARP filtering for All IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.arp_filter kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.arp_filter= To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.arp_filter = This behaviour may cause problems to system on a high availability or load balancing configuration. R12 Prevents the Linux Kernel from handling the ARP table globally. By default, the kernel may respond to an ARP request from a certain interface with information from another interface. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.arp_filter from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.arp_filter.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.arp_filter" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_arp_filter_value='' # # Set runtime for net.ipv4.conf.all.arp_filter # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.arp_filter="$sysctl_net_ipv4_conf_all_arp_filter_value" fi # # If net.ipv4.conf.all.arp_filter present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.arp_filter = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.arp_filter") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_arp_filter_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.arp_filter\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.arp_filter\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_filter - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.arp_filter.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_filter - name: Comment out any occurrences of net.ipv4.conf.all.arp_filter from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.arp_filter replace: '#net.ipv4.conf.all.arp_filter' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_filter - name: XCCDF Value sysctl_net_ipv4_conf_all_arp_filter_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_arp_filter_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.arp_filter is set ansible.posix.sysctl: name: net.ipv4.conf.all.arp_filter value: '{{ sysctl_net_ipv4_conf_all_arp_filter_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_filter Configure Response Mode of ARP Requests for All IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.arp_ignore kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.arp_ignore= To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.arp_ignore = The ARP response mode may impact behaviour of workloads and firewalls on the system. R12 Avoids ARP Flux on system that have more than one interface on the same subnet. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.arp_ignore from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.arp_ignore.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.arp_ignore" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_arp_ignore_value='' # # Set runtime for net.ipv4.conf.all.arp_ignore # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.arp_ignore="$sysctl_net_ipv4_conf_all_arp_ignore_value" fi # # If net.ipv4.conf.all.arp_ignore present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.arp_ignore = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.arp_ignore") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_arp_ignore_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.arp_ignore\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.arp_ignore\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_ignore - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.arp_ignore.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_ignore - name: Comment out any occurrences of net.ipv4.conf.all.arp_ignore from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.arp_ignore replace: '#net.ipv4.conf.all.arp_ignore' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_ignore - name: XCCDF Value sysctl_net_ipv4_conf_all_arp_ignore_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_arp_ignore_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.arp_ignore is set ansible.posix.sysctl: name: net.ipv4.conf.all.arp_ignore value: '{{ sysctl_net_ipv4_conf_all_arp_ignore_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_arp_ignore Drop Gratuitious ARP frames on All IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.drop_gratuitous_arp kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.drop_gratuitous_arp=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.drop_gratuitous_arp = 1 This can cause problems if ARP proxies are used in the network. R12 Drop Gratuitous ARP frames to prevent ARP poisoning. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.drop_gratuitous_arp from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.drop_gratuitous_arp.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.drop_gratuitous_arp" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv4.conf.all.drop_gratuitous_arp # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.drop_gratuitous_arp="1" fi # # If net.ipv4.conf.all.drop_gratuitous_arp present in /etc/sysctl.conf, change value to "1" # else, add "net.ipv4.conf.all.drop_gratuitous_arp = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.drop_gratuitous_arp") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.drop_gratuitous_arp\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.drop_gratuitous_arp\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_drop_gratuitous_arp - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.drop_gratuitous_arp.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_drop_gratuitous_arp - name: Comment out any occurrences of net.ipv4.conf.all.drop_gratuitous_arp from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.drop_gratuitous_arp replace: '#net.ipv4.conf.all.drop_gratuitous_arp' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_drop_gratuitous_arp - name: Ensure sysctl net.ipv4.conf.all.drop_gratuitous_arp is set to 1 ansible.posix.sysctl: name: net.ipv4.conf.all.drop_gratuitous_arp value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_drop_gratuitous_arp Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.log_martians kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.log_martians=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.log_martians = 1 1 11 12 13 14 15 16 2 3 7 8 9 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.04 DSS03.05 DSS05.02 DSS05.03 DSS05.05 DSS05.07 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.11.2.6 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) SC-5(3)(a) DE.CM-1 PR.AC-3 PR.DS-4 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 3.3.1.16 The presence of "martian" packets (which have impossible addresses) as well as spoofed packets, source-routed packets, and redirects could be a sign of nefarious network activity. Logging these packets enables this activity to be detected. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.log_martians from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.log_martians.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.log_martians" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_log_martians_value='' # # Set runtime for net.ipv4.conf.all.log_martians # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.log_martians="$sysctl_net_ipv4_conf_all_log_martians_value" fi # # If net.ipv4.conf.all.log_martians present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.log_martians = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.log_martians") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_log_martians_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.log_martians\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.log_martians\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_all_log_martians - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.log_martians.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_all_log_martians - unknown_severity - name: Comment out any occurrences of net.ipv4.conf.all.log_martians from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.log_martians replace: '#net.ipv4.conf.all.log_martians' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_all_log_martians - unknown_severity - name: XCCDF Value sysctl_net_ipv4_conf_all_log_martians_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_log_martians_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.log_martians is set ansible.posix.sysctl: name: net.ipv4.conf.all.log_martians value: '{{ sysctl_net_ipv4_conf_all_log_martians_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_all_log_martians - unknown_severity --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.all.log_martians%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_log_martians.conf overwrite: true Prevent Routing External Traffic to Local Loopback on All IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.route_localnet kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.route_localnet=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.route_localnet = 0 R12 Refuse the routing of packets whose source or destination address is the local loopback. This prohibits the use of network 127/8 for local routing purposes. Enabling route_localnet can expose applications listening on localhost to external traffic. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.route_localnet from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.route_localnet.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.route_localnet" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv4.conf.all.route_localnet # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.route_localnet="0" fi # # If net.ipv4.conf.all.route_localnet present in /etc/sysctl.conf, change value to "0" # else, add "net.ipv4.conf.all.route_localnet = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.route_localnet") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.route_localnet\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.route_localnet\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_route_localnet - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.route_localnet.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_route_localnet - name: Comment out any occurrences of net.ipv4.conf.all.route_localnet from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.route_localnet replace: '#net.ipv4.conf.all.route_localnet' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_route_localnet - name: Ensure sysctl net.ipv4.conf.all.route_localnet is set to 0 ansible.posix.sysctl: name: net.ipv4.conf.all.route_localnet value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_route_localnet Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.rp_filter kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.rp_filter=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.rp_filter = 1 1 12 13 14 15 16 18 2 4 6 7 8 9 APO01.06 APO13.01 BAI04.04 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.07 DSS06.02 3.1.20 4.2.3.4 4.3.3.4 4.4.3.3 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.PT-4 Req-1.4.3 SRG-OS-000480-GPOS-00227 R12 3.3.1.12 1.4.3 1.4 Enabling reverse path filtering drops packets with source addresses that should not have been able to be received on the interface they were received on. It should not be used on systems which are routers for complicated networks, but is helpful for end hosts and routers serving small networks. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.rp_filter from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.rp_filter.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.rp_filter" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_rp_filter_value='' # # Set runtime for net.ipv4.conf.all.rp_filter # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.rp_filter="$sysctl_net_ipv4_conf_all_rp_filter_value" fi # # If net.ipv4.conf.all.rp_filter present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.rp_filter = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.rp_filter") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_rp_filter_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.rp_filter\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.rp_filter\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_rp_filter - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.rp_filter.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_rp_filter - name: Comment out any occurrences of net.ipv4.conf.all.rp_filter from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.rp_filter replace: '#net.ipv4.conf.all.rp_filter' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_rp_filter - name: XCCDF Value sysctl_net_ipv4_conf_all_rp_filter_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_rp_filter_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.rp_filter is set ansible.posix.sysctl: name: net.ipv4.conf.all.rp_filter value: '{{ sysctl_net_ipv4_conf_all_rp_filter_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_rp_filter --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.all.rp_filter%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_rp_filter.conf overwrite: true Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.secure_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.secure_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.secure_redirects = 0 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 Req-1.4.3 SRG-OS-000480-GPOS-00227 R12 3.3.1.10 1.4.3 1.4 Accepting "secure" ICMP redirects (from those gateways listed as default gateways) has few legitimate uses. It should be disabled unless it is absolutely required. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.secure_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.secure_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.secure_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_secure_redirects_value='' # # Set runtime for net.ipv4.conf.all.secure_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.secure_redirects="$sysctl_net_ipv4_conf_all_secure_redirects_value" fi # # If net.ipv4.conf.all.secure_redirects present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.secure_redirects = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.secure_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_secure_redirects_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.secure_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.secure_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_secure_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.secure_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_secure_redirects - name: Comment out any occurrences of net.ipv4.conf.all.secure_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.secure_redirects replace: '#net.ipv4.conf.all.secure_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_secure_redirects - name: XCCDF Value sysctl_net_ipv4_conf_all_secure_redirects_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_secure_redirects_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.secure_redirects is set ansible.posix.sysctl: name: net.ipv4.conf.all.secure_redirects value: '{{ sysctl_net_ipv4_conf_all_secure_redirects_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_secure_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.all.secure_redirects%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_secure_redirects.conf overwrite: true Configure Sending and Accepting Shared Media Redirects for All IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.shared_media kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.shared_media= To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.shared_media = R12 This setting should be aligned with net.ipv4.conf.all.secure_redirects because it overrides it. If shared_media is enabled for an interface secure_redirects will be enabled too. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.shared_media from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.shared_media.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.shared_media" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_all_shared_media_value='' # # Set runtime for net.ipv4.conf.all.shared_media # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.shared_media="$sysctl_net_ipv4_conf_all_shared_media_value" fi # # If net.ipv4.conf.all.shared_media present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.all.shared_media = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.shared_media") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_shared_media_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.shared_media\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.shared_media\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_shared_media - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.shared_media.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_shared_media - name: Comment out any occurrences of net.ipv4.conf.all.shared_media from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.shared_media replace: '#net.ipv4.conf.all.shared_media' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_shared_media - name: XCCDF Value sysctl_net_ipv4_conf_all_shared_media_value # promote to variable set_fact: sysctl_net_ipv4_conf_all_shared_media_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.all.shared_media is set ansible.posix.sysctl: name: net.ipv4.conf.all.shared_media value: '{{ sysctl_net_ipv4_conf_all_shared_media_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_shared_media Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces To set the runtime status of the net.ipv4.conf.default.accept_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.default.accept_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.default.accept_redirects = 0 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 5.10.1.1 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 Req-1.4.3 SRG-OS-000480-GPOS-00227 R12 3.3.1.9 1.4.3 1.4 ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages modify the host's route table and are unauthenticated. An illicit ICMP redirect message could result in a man-in-the-middle attack. This feature of the IPv4 protocol has few legitimate uses. It should be disabled unless absolutely required. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.default.accept_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.accept_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.default.accept_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_default_accept_redirects_value='' # # Set runtime for net.ipv4.conf.default.accept_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.default.accept_redirects="$sysctl_net_ipv4_conf_default_accept_redirects_value" fi # # If net.ipv4.conf.default.accept_redirects present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.default.accept_redirects = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.accept_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_accept_redirects_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.accept_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.default.accept_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_redirects - name: Comment out any occurrences of net.ipv4.conf.default.accept_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.default.accept_redirects replace: '#net.ipv4.conf.default.accept_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_redirects - name: XCCDF Value sysctl_net_ipv4_conf_default_accept_redirects_value # promote to variable set_fact: sysctl_net_ipv4_conf_default_accept_redirects_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.default.accept_redirects is set ansible.posix.sysctl: name: net.ipv4.conf.default.accept_redirects value: '{{ sysctl_net_ipv4_conf_default_accept_redirects_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.default.accept_redirects%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_accept_redirects.conf overwrite: true Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces by Default To set the runtime status of the net.ipv4.conf.default.accept_source_route kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.default.accept_source_route=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.default.accept_source_route = 0 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 5.10.1.1 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 R12 3.3.1.15 Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. Accepting source-routed packets in the IPv4 protocol has few legitimate uses. It should be disabled unless it is absolutely required, such as when IPv4 forwarding is enabled and the system is legitimately functioning as a router. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.default.accept_source_route from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.accept_source_route.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.default.accept_source_route" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_default_accept_source_route_value='' # # Set runtime for net.ipv4.conf.default.accept_source_route # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.default.accept_source_route="$sysctl_net_ipv4_conf_default_accept_source_route_value" fi # # If net.ipv4.conf.default.accept_source_route present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.default.accept_source_route = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.accept_source_route") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_accept_source_route_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.accept_source_route\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_source_route - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.default.accept_source_route.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_source_route - name: Comment out any occurrences of net.ipv4.conf.default.accept_source_route from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.default.accept_source_route replace: '#net.ipv4.conf.default.accept_source_route' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_source_route - name: XCCDF Value sysctl_net_ipv4_conf_default_accept_source_route_value # promote to variable set_fact: sysctl_net_ipv4_conf_default_accept_source_route_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.default.accept_source_route is set ansible.posix.sysctl: name: net.ipv4.conf.default.accept_source_route value: '{{ sysctl_net_ipv4_conf_default_accept_source_route_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_accept_source_route --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.default.accept_source_route%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_accept_source_route.conf overwrite: true Enable Kernel Paremeter to Log Martian Packets on all IPv4 Interfaces by Default To set the runtime status of the net.ipv4.conf.default.log_martians kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.default.log_martians=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.default.log_martians = 1 1 11 12 13 14 15 16 2 3 7 8 9 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.04 DSS03.05 DSS05.02 DSS05.03 DSS05.05 DSS05.07 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.11.2.6 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) SC-5(3)(a) DE.CM-1 PR.AC-3 PR.DS-4 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 3.3.1.17 The presence of "martian" packets (which have impossible addresses) as well as spoofed packets, source-routed packets, and redirects could be a sign of nefarious network activity. Logging these packets enables this activity to be detected. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.default.log_martians from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.log_martians.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.default.log_martians" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_default_log_martians_value='' # # Set runtime for net.ipv4.conf.default.log_martians # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.default.log_martians="$sysctl_net_ipv4_conf_default_log_martians_value" fi # # If net.ipv4.conf.default.log_martians present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.default.log_martians = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.log_martians") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_log_martians_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.log_martians\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.log_martians\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_default_log_martians - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.default.log_martians.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_default_log_martians - unknown_severity - name: Comment out any occurrences of net.ipv4.conf.default.log_martians from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.default.log_martians replace: '#net.ipv4.conf.default.log_martians' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_default_log_martians - unknown_severity - name: XCCDF Value sysctl_net_ipv4_conf_default_log_martians_value # promote to variable set_fact: sysctl_net_ipv4_conf_default_log_martians_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.default.log_martians is set ansible.posix.sysctl: name: net.ipv4.conf.default.log_martians value: '{{ sysctl_net_ipv4_conf_default_log_martians_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(3)(a) - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_conf_default_log_martians - unknown_severity --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.default.log_martians%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_log_martians.conf overwrite: true Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces by Default To set the runtime status of the net.ipv4.conf.default.rp_filter kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.default.rp_filter=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.default.rp_filter = 1 1 12 13 14 15 16 18 2 4 6 7 8 9 APO01.06 APO13.01 BAI04.04 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.07 DSS06.02 3.1.20 4.2.3.4 4.3.3.4 4.4.3.3 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.PT-4 SRG-OS-000480-GPOS-00227 R12 3.3.1.13 Enabling reverse path filtering drops packets with source addresses that should not have been able to be received on the interface they were received on. It should not be used on systems which are routers for complicated networks, but is helpful for end hosts and routers serving small networks. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.default.rp_filter from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.rp_filter.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.default.rp_filter" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_default_rp_filter_value='' # # Set runtime for net.ipv4.conf.default.rp_filter # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.default.rp_filter="$sysctl_net_ipv4_conf_default_rp_filter_value" fi # # If net.ipv4.conf.default.rp_filter present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.default.rp_filter = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.rp_filter") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_rp_filter_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.rp_filter\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.rp_filter\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_rp_filter - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.default.rp_filter.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_rp_filter - name: Comment out any occurrences of net.ipv4.conf.default.rp_filter from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.default.rp_filter replace: '#net.ipv4.conf.default.rp_filter' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_rp_filter - name: XCCDF Value sysctl_net_ipv4_conf_default_rp_filter_value # promote to variable set_fact: sysctl_net_ipv4_conf_default_rp_filter_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.default.rp_filter is set ansible.posix.sysctl: name: net.ipv4.conf.default.rp_filter value: '{{ sysctl_net_ipv4_conf_default_rp_filter_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_rp_filter --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.default.rp_filter%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_rp_filter.conf overwrite: true Configure Kernel Parameter for Accepting Secure Redirects By Default To set the runtime status of the net.ipv4.conf.default.secure_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.default.secure_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.default.secure_redirects = 0 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 R12 3.3.1.10 3.3.1.11 Accepting "secure" ICMP redirects (from those gateways listed as default gateways) has few legitimate uses. It should be disabled unless it is absolutely required. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.default.secure_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.secure_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.default.secure_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_default_secure_redirects_value='' # # Set runtime for net.ipv4.conf.default.secure_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.default.secure_redirects="$sysctl_net_ipv4_conf_default_secure_redirects_value" fi # # If net.ipv4.conf.default.secure_redirects present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.default.secure_redirects = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.secure_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_secure_redirects_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.secure_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.secure_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_secure_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.default.secure_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_secure_redirects - name: Comment out any occurrences of net.ipv4.conf.default.secure_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.default.secure_redirects replace: '#net.ipv4.conf.default.secure_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_secure_redirects - name: XCCDF Value sysctl_net_ipv4_conf_default_secure_redirects_value # promote to variable set_fact: sysctl_net_ipv4_conf_default_secure_redirects_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.default.secure_redirects is set ansible.posix.sysctl: name: net.ipv4.conf.default.secure_redirects value: '{{ sysctl_net_ipv4_conf_default_secure_redirects_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_secure_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.default.secure_redirects%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_secure_redirects.conf overwrite: true Configure Sending and Accepting Shared Media Redirects by Default To set the runtime status of the net.ipv4.conf.default.shared_media kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.default.shared_media= To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.default.shared_media = R12 This setting should be aligned with net.ipv4.conf.default.secure_redirects because it overrides it. If shared_media is enabled for an interface secure_redirects will be enabled too. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.default.shared_media from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.shared_media.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.default.shared_media" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_conf_default_shared_media_value='' # # Set runtime for net.ipv4.conf.default.shared_media # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.default.shared_media="$sysctl_net_ipv4_conf_default_shared_media_value" fi # # If net.ipv4.conf.default.shared_media present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.conf.default.shared_media = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.shared_media") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_shared_media_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.shared_media\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.shared_media\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_shared_media - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.default.shared_media.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_shared_media - name: Comment out any occurrences of net.ipv4.conf.default.shared_media from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.default.shared_media replace: '#net.ipv4.conf.default.shared_media' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_shared_media - name: XCCDF Value sysctl_net_ipv4_conf_default_shared_media_value # promote to variable set_fact: sysctl_net_ipv4_conf_default_shared_media_value: !!str tags: - always - name: Ensure sysctl net.ipv4.conf.default.shared_media is set ansible.posix.sysctl: name: net.ipv4.conf.default.shared_media value: '{{ sysctl_net_ipv4_conf_default_shared_media_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_shared_media Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces To set the runtime status of the net.ipv4.icmp_echo_ignore_broadcasts kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.icmp_echo_ignore_broadcasts = 1 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 5.10.1.1 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 Req-1.4.3 SRG-OS-000480-GPOS-00227 3.3.1.7 1.4.2 1.4 Responding to broadcast (ICMP) echoes facilitates network mapping and provides a vector for amplification attacks. Ignoring ICMP echo requests (pings) sent to broadcast or multicast addresses makes the system slightly more difficult to enumerate on the network. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.icmp_echo_ignore_broadcasts from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.icmp_echo_ignore_broadcasts.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.icmp_echo_ignore_broadcasts" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value='' # # Set runtime for net.ipv4.icmp_echo_ignore_broadcasts # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.icmp_echo_ignore_broadcasts="$sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value" fi # # If net.ipv4.icmp_echo_ignore_broadcasts present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.icmp_echo_ignore_broadcasts = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.icmp_echo_ignore_broadcasts") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.icmp_echo_ignore_broadcasts\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.icmp_echo_ignore_broadcasts\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_icmp_echo_ignore_broadcasts - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.icmp_echo_ignore_broadcasts.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_icmp_echo_ignore_broadcasts - name: Comment out any occurrences of net.ipv4.icmp_echo_ignore_broadcasts from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.icmp_echo_ignore_broadcasts replace: '#net.ipv4.icmp_echo_ignore_broadcasts' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_icmp_echo_ignore_broadcasts - name: XCCDF Value sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value # promote to variable set_fact: sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value: !!str tags: - always - name: Ensure sysctl net.ipv4.icmp_echo_ignore_broadcasts is set ansible.posix.sysctl: name: net.ipv4.icmp_echo_ignore_broadcasts value: '{{ sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_icmp_echo_ignore_broadcasts --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.icmp_echo_ignore_broadcasts%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_icmp_echo_ignore_broadcasts.conf overwrite: true Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces To set the runtime status of the net.ipv4.icmp_ignore_bogus_error_responses kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.icmp_ignore_bogus_error_responses = 1 1 11 12 13 14 15 16 2 3 7 8 9 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS05.02 DSS05.05 DSS05.07 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.9.1.2 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 DE.CM-1 PR.DS-4 PR.IP-1 PR.PT-3 Req-1.4.3 SRG-OS-000480-GPOS-00227 R12 3.3.1.6 1.4.2 1.4 Ignoring bogus ICMP error responses reduces log size, although some activity would not be logged. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.icmp_ignore_bogus_error_responses from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.icmp_ignore_bogus_error_responses.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.icmp_ignore_bogus_error_responses" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value='' # # Set runtime for net.ipv4.icmp_ignore_bogus_error_responses # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.icmp_ignore_bogus_error_responses="$sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value" fi # # If net.ipv4.icmp_ignore_bogus_error_responses present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.icmp_ignore_bogus_error_responses = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.icmp_ignore_bogus_error_responses") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.icmp_ignore_bogus_error_responses\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.icmp_ignore_bogus_error_responses\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_icmp_ignore_bogus_error_responses - unknown_severity - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.icmp_ignore_bogus_error_responses.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_icmp_ignore_bogus_error_responses - unknown_severity - name: Comment out any occurrences of net.ipv4.icmp_ignore_bogus_error_responses from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.icmp_ignore_bogus_error_responses replace: '#net.ipv4.icmp_ignore_bogus_error_responses' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_icmp_ignore_bogus_error_responses - unknown_severity - name: XCCDF Value sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value # promote to variable set_fact: sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value: !!str tags: - always - name: Ensure sysctl net.ipv4.icmp_ignore_bogus_error_responses is set ansible.posix.sysctl: name: net.ipv4.icmp_ignore_bogus_error_responses value: '{{ sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - PCI-DSS-Req-1.4.3 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - low_complexity - medium_disruption - reboot_required - sysctl_net_ipv4_icmp_ignore_bogus_error_responses - unknown_severity --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.icmp_ignore_bogus_error_responses%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_icmp_ignore_bogus_error_responses.conf overwrite: true Set Kernel Parameter to Increase Local Port Range To set the runtime status of the net.ipv4.ip_local_port_range kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.ip_local_port_range=32768 65535 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.ip_local_port_range = 32768 65535 R12 This setting defines the local port range that is used by TCP and UDP to choose the local port. The first number is the first, the second the last local port number. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.ip_local_port_range from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.ip_local_port_range.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.ip_local_port_range" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv4.ip_local_port_range # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.ip_local_port_range="32768 65535" fi # # If net.ipv4.ip_local_port_range present in /etc/sysctl.conf, change value to "32768 65535" # else, add "net.ipv4.ip_local_port_range = 32768 65535" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.ip_local_port_range") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "32768 65535" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.ip_local_port_range\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.ip_local_port_range\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_local_port_range - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.ip_local_port_range.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_local_port_range - name: Comment out any occurrences of net.ipv4.ip_local_port_range from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.ip_local_port_range replace: '#net.ipv4.ip_local_port_range' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_local_port_range - name: Ensure sysctl net.ipv4.ip_local_port_range is set to 32768 65535 ansible.posix.sysctl: name: net.ipv4.ip_local_port_range value: 32768 65535 sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_local_port_range Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces To set the runtime status of the net.ipv4.tcp_syncookies kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.tcp_syncookies=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.tcp_syncookies = 1 1 12 13 14 15 16 18 2 4 6 7 8 9 5.10.1.1 APO01.06 APO13.01 BAI04.04 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.07 DSS06.02 3.1.20 4.2.3.4 4.3.3.4 4.4.3.3 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) SC-5(1) SC-5(2) SC-5(3)(a) CM-6(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.PT-4 Req-1.4.1 SRG-OS-000480-GPOS-00227 SRG-OS-000420-GPOS-00186 SRG-OS-000142-GPOS-00071 R12 3.3.1.18 1.4.3 1.4 A TCP SYN flood attack can cause a denial of service by filling a system's TCP connection table with connections in the SYN_RCVD state. Syncookies can be used to track a connection when a subsequent ACK is received, verifying the initiator is attempting a valid connection and is not a flood source. This feature is activated when a flood condition is detected, and enables the system to continue servicing valid connection requests. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.tcp_syncookies from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.tcp_syncookies.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.tcp_syncookies" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_net_ipv4_tcp_syncookies_value='' # # Set runtime for net.ipv4.tcp_syncookies # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.tcp_syncookies="$sysctl_net_ipv4_tcp_syncookies_value" fi # # If net.ipv4.tcp_syncookies present in /etc/sysctl.conf, change value to appropriate value # else, add "net.ipv4.tcp_syncookies = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.tcp_syncookies") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_tcp_syncookies_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.tcp_syncookies\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.tcp_syncookies\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(1) - NIST-800-53-SC-5(2) - NIST-800-53-SC-5(3)(a) - PCI-DSS-Req-1.4.1 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_tcp_syncookies - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.tcp_syncookies.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(1) - NIST-800-53-SC-5(2) - NIST-800-53-SC-5(3)(a) - PCI-DSS-Req-1.4.1 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_tcp_syncookies - name: Comment out any occurrences of net.ipv4.tcp_syncookies from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.tcp_syncookies replace: '#net.ipv4.tcp_syncookies' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(1) - NIST-800-53-SC-5(2) - NIST-800-53-SC-5(3)(a) - PCI-DSS-Req-1.4.1 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_tcp_syncookies - name: XCCDF Value sysctl_net_ipv4_tcp_syncookies_value # promote to variable set_fact: sysctl_net_ipv4_tcp_syncookies_value: !!str tags: - always - name: Ensure sysctl net.ipv4.tcp_syncookies is set ansible.posix.sysctl: name: net.ipv4.tcp_syncookies value: '{{ sysctl_net_ipv4_tcp_syncookies_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5(1) - NIST-800-53-SC-5(2) - NIST-800-53-SC-5(3)(a) - PCI-DSS-Req-1.4.1 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_tcp_syncookies --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.tcp_syncookies%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_tcp_syncookies.conf overwrite: true Network Parameters for Hosts Only If the system is not going to be used as a router, then setting certain kernel parameters ensure that the host will not perform routing of network traffic. Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces To set the runtime status of the net.ipv4.conf.all.send_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.all.send_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.all.send_redirects = 0 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 5.10.1.1 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 CM-6(a) SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 R12 3.3.1.4 1.4.5 1.4 ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages contain information from the system's route table possibly revealing portions of the network topology. The ability to send ICMP redirects is only appropriate for systems acting as routers. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.all.send_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.send_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.all.send_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv4.conf.all.send_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.all.send_redirects="0" fi # # If net.ipv4.conf.all.send_redirects present in /etc/sysctl.conf, change value to "0" # else, add "net.ipv4.conf.all.send_redirects = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.send_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.send_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.send_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_send_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.all.send_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_send_redirects - name: Comment out any occurrences of net.ipv4.conf.all.send_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.all.send_redirects replace: '#net.ipv4.conf.all.send_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_send_redirects - name: Ensure sysctl net.ipv4.conf.all.send_redirects is set to 0 ansible.posix.sysctl: name: net.ipv4.conf.all.send_redirects value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_all_send_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.all.send_redirects%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_send_redirects.conf overwrite: true Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces by Default To set the runtime status of the net.ipv4.conf.default.send_redirects kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.conf.default.send_redirects=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.conf.default.send_redirects = 0 1 11 12 13 14 15 16 18 2 3 4 6 7 8 9 5.10.1.1 APO01.06 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS01.05 DSS03.01 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 3.1.20 4.2.3.4 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 CM-6(a) SC-7(a) DE.AE-1 DE.CM-1 ID.AM-3 PR.AC-5 PR.DS-4 PR.DS-5 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 R12 3.3.1.5 1.4.5 1.4 ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages contain information from the system's route table possibly revealing portions of the network topology. The ability to send ICMP redirects is only appropriate for systems acting as routers. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.conf.default.send_redirects from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.send_redirects.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.conf.default.send_redirects" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv4.conf.default.send_redirects # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.conf.default.send_redirects="0" fi # # If net.ipv4.conf.default.send_redirects present in /etc/sysctl.conf, change value to "0" # else, add "net.ipv4.conf.default.send_redirects = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.send_redirects") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.send_redirects\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.send_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_send_redirects - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.conf.default.send_redirects.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_send_redirects - name: Comment out any occurrences of net.ipv4.conf.default.send_redirects from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.conf.default.send_redirects replace: '#net.ipv4.conf.default.send_redirects' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_send_redirects - name: Ensure sysctl net.ipv4.conf.default.send_redirects is set to 0 ansible.posix.sysctl: name: net.ipv4.conf.default.send_redirects value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1.1 - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.5 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_conf_default_send_redirects --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.ipv4.conf.default.send_redirects%3D0%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_send_redirects.conf overwrite: true Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces To set the runtime status of the net.ipv4.ip_forward kernel parameter, run the following command: $ sudo sysctl -w net.ipv4.ip_forward=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.ipv4.ip_forward = 0 Certain technologies such as virtual machines, containers, etc. rely on IPv4 forwarding to enable and use networking. Disabling IPv4 forwarding would cause those technologies to stop working. Therefore, this rule should not be used in profiles or benchmarks that target usage of IPv4 forwarding. 1 11 12 13 14 15 16 2 3 7 8 9 APO13.01 BAI04.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS05.02 DSS05.05 DSS05.07 DSS06.06 3.1.20 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.2 SR 7.1 SR 7.2 SR 7.6 A.12.1.2 A.12.1.3 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.17.2.1 A.9.1.2 CIP-007-3 R4 CIP-007-3 R4.1 CIP-007-3 R4.2 CIP-007-3 R5.1 CM-7(a) CM-7(b) SC-5 CM-6(a) SC-7(a) DE.CM-1 PR.DS-4 PR.IP-1 PR.PT-3 PR.PT-4 Req-1.3.1 Req-1.3.2 SRG-OS-000480-GPOS-00227 R12 3.3.1.1 1.4.3 1.4 Routing protocol daemons are typically used on routers to exchange network topology information with other routers. If this capability is used when not required, system network information may be unnecessarily transmitted across the network. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.ipv4.ip_forward from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.ip_forward.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.ipv4.ip_forward" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.ipv4.ip_forward # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.ipv4.ip_forward="0" fi # # If net.ipv4.ip_forward present in /etc/sysctl.conf, change value to "0" # else, add "net.ipv4.ip_forward = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.ip_forward") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.ip_forward\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.ip_forward\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.3.1 - PCI-DSS-Req-1.3.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_forward - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.ipv4.ip_forward.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.3.1 - PCI-DSS-Req-1.3.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_forward - name: Comment out any occurrences of net.ipv4.ip_forward from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.ipv4.ip_forward replace: '#net.ipv4.ip_forward' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.3.1 - PCI-DSS-Req-1.3.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_forward - name: Ensure sysctl net.ipv4.ip_forward is set to 0 ansible.posix.sysctl: name: net.ipv4.ip_forward value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.20 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-SC-5 - NIST-800-53-SC-7(a) - PCI-DSS-Req-1.3.1 - PCI-DSS-Req-1.3.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.3 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_ipv4_ip_forward nftables If firewalld or iptables are being used in your environment, please follow the guidance in their respective section and pass-over the guidance in this section. nftables is a subsystem of the Linux kernel providing filtering and classification of network packets/datagrams/frames and is the successor to iptables. The biggest change with the successor nftables is its simplicity. With iptables, we have to configure every single rule and use the syntax which can be compared with normal commands. With nftables, the simpler syntax, much like BPF (Berkely Packet Filter) means shorter lines and less repetition. Support for nftables should also be compiled into the kernel, together with the related nftables modules. It is available in Linux kernels >= 3.13. Please ensure that your kernel supports nftables before choosing this option. Install nftables Package nftables provides a new in-kernel packet classification framework that is based on a network-specific Virtual Machine (VM) and a new nft userspace command line tool. nftables reuses the existing Netfilter subsystems such as the existing hook infrastructure, the connection tracking system, NAT, userspace queuing and logging subsystem. The nftables package can be installed with the following command: $ sudo dnf install nftables 4.1.1 1.2.1 1.2 nftables is a subsystem of the Linux kernel that can protect against threats originating from within a corporate network to include malicious mobile code and poorly configured software on a host. # Remediation is applicable only in certain platforms if ( ! (systemctl is-active iptables &>/dev/null) && ! (systemctl is-active ufw &>/dev/null) && rpm --quiet -q kernel ); then if ! rpm -q --quiet "nftables" ; then dnf install -y "nftables" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_nftables_installed - name: Ensure nftables is installed ansible.builtin.package: name: nftables state: present when: ( "kernel" in ansible_facts.packages ) tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_nftables_installed include install_nftables class install_nftables { package { 'nftables': ensure => 'installed', } } package --add=nftables [[packages]] name = "nftables" version = "*" package install nftables dnf install nftables Verify nftables Service is Disabled nftables is a subsystem of the Linux kernel providing filtering and classification of network packets/datagrams/frames and is the successor to iptables. The nftables service can be disabled with the following command: systemctl disable nftables 4.1.2 1.2.1 1.2 Running both firewalld and nftables may lead to conflict. nftables is actually one of the backends for firewalld management tools. # Remediation is applicable only in certain platforms if ( rpm --quiet -q firewalld && rpm --quiet -q nftables && rpm --quiet -q kernel ); then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'nftables.service' fi "$SYSTEMCTL_EXEC" disable 'nftables.service' "$SYSTEMCTL_EXEC" mask 'nftables.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files nftables.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'nftables.socket' fi "$SYSTEMCTL_EXEC" mask 'nftables.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'nftables.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_nftables_disabled - name: Verify nftables Service is Disabled - Disable service nftables block: - name: Verify nftables Service is Disabled - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Verify nftables Service is Disabled - Ensure nftables.service is Masked ansible.builtin.systemd: name: nftables.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("nftables.service", multiline=True) - name: Unit Socket Exists - nftables.socket ansible.builtin.command: systemctl -q list-unit-files nftables.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Verify nftables Service is Disabled - Disable Socket nftables ansible.builtin.systemd: name: nftables.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("nftables.socket", multiline=True) tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.1 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_nftables_disabled - special_service_block when: ( "firewalld" in ansible_facts.packages and "nftables" in ansible_facts.packages and "kernel" in ansible_facts.packages ) include disable_nftables class disable_nftables { service {'nftables': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: nftables.service enabled: false mask: true - name: nftables.socket enabled: false mask: true [customizations.services] masked = ["nftables"] service disable nftables Uncomplicated Firewall (ufw) The Linux kernel in Ubuntu provides a packet filtering system called netfilter, and the traditional interface for manipulating netfilter are the iptables suite of commands. iptables provide a complete firewall solution that is both highly configurable and highly flexible. Becoming proficient in iptables takes time, and getting started with netfilter firewalling using only iptables can be a daunting task. As a result, many frontends for iptables have been created over the years, each trying to achieve a different result and targeting a different audience. The Uncomplicated Firewall (ufw) is a frontend for iptables and is particularly well-suited for host-based firewalls. ufw provides a framework for managing netfilter, as well as a command-line interface for manipulating the firewall. ufw aims to provide an easy to use interface for people unfamiliar with firewall concepts, while at the same time simplifies complicated iptables commands to help an administrator who knows what he or she is doing. ufw is an upstream for other distributions and graphical frontends. Verify ufw Enabled The ufw service can be enabled with the following command: $ sudo systemctl enable ufw.service SRG-OS-000297-GPOS-00115 The ufw service must be enabled and running in order for ufw to protect the system # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { ( rpm --quiet -q ufw && rpm --quiet -q kernel ); }; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'ufw.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'ufw.service' fi "$SYSTEMCTL_EXEC" enable 'ufw.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_ufw_enabled - name: Verify ufw Enabled - Enable service ufw block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Verify ufw Enabled - Enable Service ufw ansible.builtin.systemd: name: ufw enabled: true state: started masked: false when: - '"ufw" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_ufw_enabled - special_service_block when: - '"kernel" in ansible_facts.packages' - ( "ufw" in ansible_facts.packages and "kernel" in ansible_facts.packages ) include enable_ufw class enable_ufw { service {'ufw': enable => true, ensure => 'running', } } [customizations.services] enabled = ["ufw"] service enable ufw Uncommon Network Protocols The system includes support for several network protocols which are not commonly used. Although security vulnerabilities in kernel networking code are not frequently discovered, the consequences can be dramatic. Ensuring uncommon network protocols are disabled reduces the system's risk to attacks targeted at its implementation of those protocols. Although these protocols are not commonly used, avoid disruption in your network environment by ensuring they are not needed prior to disabling them. Disable ATM Support The Asynchronous Transfer Mode (ATM) is a protocol operating on network, data link, and physical layers, based on virtual circuits and virtual paths. To configure the system to prevent the atm kernel module from being loaded, add the following line to the file /etc/modprobe.d/atm.conf: install atm /bin/false AC-18 SRG-OS-000095-GPOS-00049 SRG-OS-000480-GPOS-00227 Disabling ATM protects the system against exploitation of any flaws in its implementation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install atm" /etc/modprobe.d/atm.conf ; then sed -i 's#^install atm.*#install atm /bin/false#g' /etc/modprobe.d/atm.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/atm.conf echo "install atm /bin/false" >> /etc/modprobe.d/atm.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-18 - disable_strategy - kernel_module_atm_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'atm' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/atm.conf regexp: install\s+atm line: install atm /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-18 - disable_strategy - kernel_module_atm_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20atm%20/bin/false%0Ablacklist%20atm%0A mode: 0644 path: /etc/modprobe.d/atm.conf overwrite: true Disable CAN Support The Controller Area Network (CAN) is a serial communications protocol which was initially developed for automotive and is now also used in marine, industrial, and medical applications. To configure the system to prevent the can kernel module from being loaded, add the following line to the file /etc/modprobe.d/can.conf: install can /bin/false AC-18 FMT_SMF_EXT.1 SRG-OS-000095-GPOS-00049 SRG-OS-000480-GPOS-00227 Disabling CAN protects the system against exploitation of any flaws in its implementation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install can" /etc/modprobe.d/can.conf ; then sed -i 's#^install can.*#install can /bin/false#g' /etc/modprobe.d/can.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/can.conf echo "install can /bin/false" >> /etc/modprobe.d/can.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-18 - disable_strategy - kernel_module_can_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'can' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/can.conf regexp: install\s+can line: install can /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-18 - disable_strategy - kernel_module_can_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20can%20/bin/false%0Ablacklist%20can%0A mode: 0644 path: /etc/modprobe.d/can.conf overwrite: true Disable DCCP Support The Datagram Congestion Control Protocol (DCCP) is a relatively new transport layer protocol, designed to support streaming media and telephony. To configure the system to prevent the dccp kernel module from being loaded, add the following line to the file /etc/modprobe.d/dccp.conf: install dccp /bin/false 11 14 3 9 5.10.1 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Req-1.4.2 SRG-OS-000096-GPOS-00050 SRG-OS-000378-GPOS-00163 1.4.2 1.4 Disabling DCCP protects the system against exploitation of any flaws in its implementation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install dccp" /etc/modprobe.d/dccp.conf ; then sed -i 's#^install dccp.*#install dccp /bin/false#g' /etc/modprobe.d/dccp.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/dccp.conf echo "install dccp /bin/false" >> /etc/modprobe.d/dccp.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1 - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - kernel_module_dccp_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'dccp' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/dccp.conf regexp: install\s+dccp line: install dccp /bin/false when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1 - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - kernel_module_dccp_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20dccp%20/bin/false%0Ablacklist%20dccp%0A mode: 0644 path: /etc/modprobe.d/dccp.conf overwrite: true Disable IEEE 1394 (FireWire) Support The IEEE 1394 (FireWire) is a serial bus standard for high-speed real-time communication. To configure the system to prevent the firewire-core kernel module from being loaded, add the following line to the file /etc/modprobe.d/firewire-core.conf: install firewire-core /bin/false AC-18 SRG-OS-000095-GPOS-00049 1.1.1.9 Disabling FireWire protects the system against exploitation of any flaws in its implementation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install firewire-core" /etc/modprobe.d/firewire-core.conf ; then sed -i 's#^install firewire-core.*#install firewire-core /bin/false#g' /etc/modprobe.d/firewire-core.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/firewire-core.conf echo "install firewire-core /bin/false" >> /etc/modprobe.d/firewire-core.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-18 - disable_strategy - kernel_module_firewire-core_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'firewire-core' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/firewire-core.conf regexp: install\s+firewire-core line: install firewire-core /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-18 - disable_strategy - kernel_module_firewire-core_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20firewire-core%20/bin/false%0Ablacklist%20firewire-core%0A mode: 0644 path: /etc/modprobe.d/firewire-core.conf overwrite: true Disable RDS Support The Reliable Datagram Sockets (RDS) protocol is a transport layer protocol designed to provide reliable high-bandwidth, low-latency communications between nodes in a cluster. To configure the system to prevent the rds kernel module from being loaded, add the following line to the file /etc/modprobe.d/rds.conf: install rds /bin/false 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Disabling RDS protects the system against exploitation of any flaws in its implementation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install rds" /etc/modprobe.d/rds.conf ; then sed -i 's#^install rds.*#install rds /bin/false#g' /etc/modprobe.d/rds.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/rds.conf echo "install rds /bin/false" >> /etc/modprobe.d/rds.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_rds_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'rds' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/rds.conf regexp: install\s+rds line: install rds /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_rds_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20rds%20/bin/false%0Ablacklist%20rds%0A mode: 0644 path: /etc/modprobe.d/rds.conf overwrite: true Disable SCTP Support The Stream Control Transmission Protocol (SCTP) is a transport layer protocol, designed to support the idea of message-oriented communication, with several streams of messages within one connection. To configure the system to prevent the sctp kernel module from being loaded, add the following line to the file /etc/modprobe.d/sctp.conf: install sctp /bin/false 11 14 3 9 5.10.1 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 FMT_SMF_EXT.1 Req-1.4.2 SRG-OS-000095-GPOS-00049 SRG-OS-000480-GPOS-00227 3.2.6 1.4.2 1.4 Disabling SCTP protects the system against exploitation of any flaws in its implementation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install sctp" /etc/modprobe.d/sctp.conf ; then sed -i 's#^install sctp.*#install sctp /bin/false#g' /etc/modprobe.d/sctp.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/sctp.conf echo "install sctp /bin/false" >> /etc/modprobe.d/sctp.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.10.1 - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - kernel_module_sctp_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'sctp' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/sctp.conf regexp: install\s+sctp line: install sctp /bin/false when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.10.1 - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-1.4.2 - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - disable_strategy - kernel_module_sctp_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20sctp%20/bin/false%0Ablacklist%20sctp%0A mode: 0644 path: /etc/modprobe.d/sctp.conf overwrite: true Disable TIPC Support The Transparent Inter-Process Communication (TIPC) protocol is designed to provide communications between nodes in a cluster. To configure the system to prevent the tipc kernel module from being loaded, add the following line to the file /etc/modprobe.d/tipc.conf: install tipc /bin/false This configuration baseline was created to deploy the base operating system for general purpose workloads. When the operating system is configured for certain purposes, such as a node in High Performance Computing cluster, it is expected that the tipc kernel module will be loaded. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 FMT_SMF_EXT.1 SRG-OS-000095-GPOS-00049 3.2.4 Disabling TIPC protects the system against exploitation of any flaws in its implementation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install tipc" /etc/modprobe.d/tipc.conf ; then sed -i 's#^install tipc.*#install tipc /bin/false#g' /etc/modprobe.d/tipc.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/tipc.conf echo "install tipc /bin/false" >> /etc/modprobe.d/tipc.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_tipc_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'tipc' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/tipc.conf regexp: install\s+tipc line: install tipc /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_tipc_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20tipc%20/bin/false%0Ablacklist%20tipc%0A mode: 0644 path: /etc/modprobe.d/tipc.conf overwrite: true Wireless Networking Wireless networking, such as 802.11 (WiFi) and Bluetooth, can present a security risk to sensitive or classified systems and networks. Wireless networking hardware is much more likely to be included in laptop or portable systems than in desktops or servers. Removal of hardware provides the greatest assurance that the wireless capability remains disabled. Acquisition policies often include provisions to prevent the purchase of equipment that will be used in sensitive spaces and includes wireless capabilities. If it is impractical to remove the wireless hardware, and policy permits the device to enter sensitive spaces as long as wireless is disabled, efforts should instead focus on disabling wireless capability via software. Disable Wireless Through Software Configuration If it is impossible to remove the wireless hardware from the device in question, disable as much of it as possible through software. The following methods can disable software support for wireless networking, but note that these methods do not prevent malicious software or careless users from re-activating the devices. Disable Bluetooth Service The bluetooth service can be disabled with the following command: $ sudo systemctl mask --now bluetooth.service $ sudo service bluetooth stop 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 3.1.16 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 3.1.3 Disabling the bluetooth service prevents the system from attempting connections to Bluetooth devices, which entails some security risk. Nevertheless, variation in this risk decision may be expected due to the utility of Bluetooth connectivity and its limited range. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'bluetooth.service' fi "$SYSTEMCTL_EXEC" disable 'bluetooth.service' "$SYSTEMCTL_EXEC" mask 'bluetooth.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files bluetooth.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'bluetooth.socket' fi "$SYSTEMCTL_EXEC" mask 'bluetooth.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'bluetooth.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_bluetooth_disabled - name: Disable Bluetooth Service - Disable service bluetooth block: - name: Disable Bluetooth Service - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable Bluetooth Service - Ensure bluetooth.service is Masked ansible.builtin.systemd: name: bluetooth.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("bluetooth.service", multiline=True) - name: Unit Socket Exists - bluetooth.socket ansible.builtin.command: systemctl -q list-unit-files bluetooth.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable Bluetooth Service - Disable Socket bluetooth ansible.builtin.systemd: name: bluetooth.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("bluetooth.socket", multiline=True) tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_bluetooth_disabled - special_service_block when: '"kernel" in ansible_facts.packages' include disable_bluetooth class disable_bluetooth { service {'bluetooth': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: bluetooth.service enabled: false mask: true - name: bluetooth.socket enabled: false mask: true [customizations.services] masked = ["bluetooth"] service disable bluetooth Disable Bluetooth Kernel Module The kernel's module loading system can be configured to prevent loading of the Bluetooth module. Add the following to the appropriate /etc/modprobe.d configuration file to prevent the loading of the Bluetooth module: install bluetooth /bin/true 11 12 14 15 3 8 9 5.13.1.3 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 3.1.16 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 FMT_SMF_EXT.1 SRG-OS-000095-GPOS-00049 SRG-OS-000300-GPOS-00118 If Bluetooth functionality must be disabled, preventing the kernel from loading the kernel module provides an additional safeguard against its activation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install bluetooth" /etc/modprobe.d/bluetooth.conf ; then sed -i 's#^install bluetooth.*#install bluetooth /bin/false#g' /etc/modprobe.d/bluetooth.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/bluetooth.conf echo "install bluetooth /bin/false" >> /etc/modprobe.d/bluetooth.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.13.1.3 - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_bluetooth_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'bluetooth' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/bluetooth.conf regexp: install\s+bluetooth line: install bluetooth /bin/false when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.13.1.3 - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_bluetooth_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20bluetooth%20/bin/false%0Ablacklist%20bluetooth%0A mode: 0644 path: /etc/modprobe.d/bluetooth.conf overwrite: true Disable Kernel cfg80211 Module To configure the system to prevent the cfg80211 kernel module from being loaded, add the following line to the file /etc/modprobe.d/cfg80211.conf: install cfg80211 /bin/false AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 AC-18(4) If Wireless functionality must be disabled, preventing the kernel from loading the kernel module provides an additional safeguard against its activation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install cfg80211" /etc/modprobe.d/cfg80211.conf ; then sed -i 's#^install cfg80211.*#install cfg80211 /bin/false#g' /etc/modprobe.d/cfg80211.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/cfg80211.conf echo "install cfg80211 /bin/false" >> /etc/modprobe.d/cfg80211.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_cfg80211_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'cfg80211' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/cfg80211.conf regexp: install\s+cfg80211 line: install cfg80211 /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_cfg80211_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20cfg80211%20/bin/false%0Ablacklist%20cfg80211%0A mode: 0644 path: /etc/modprobe.d/cfg80211.conf overwrite: true Disable Kernel iwlmvm Module To configure the system to prevent the iwlmvm kernel module from being loaded, add the following line to the file /etc/modprobe.d/iwlmvm.conf: install iwlmvm /bin/false AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 AC-18(4) If Wireless functionality must be disabled, preventing the kernel from loading the kernel module provides an additional safeguard against its activation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install iwlmvm" /etc/modprobe.d/iwlmvm.conf ; then sed -i 's#^install iwlmvm.*#install iwlmvm /bin/false#g' /etc/modprobe.d/iwlmvm.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/iwlmvm.conf echo "install iwlmvm /bin/false" >> /etc/modprobe.d/iwlmvm.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_iwlmvm_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'iwlmvm' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/iwlmvm.conf regexp: install\s+iwlmvm line: install iwlmvm /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_iwlmvm_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20iwlmvm%20/bin/false%0Ablacklist%20iwlmvm%0A mode: 0644 path: /etc/modprobe.d/iwlmvm.conf overwrite: true Disable Kernel iwlwifi Module To configure the system to prevent the iwlwifi kernel module from being loaded, add the following line to the file /etc/modprobe.d/iwlwifi.conf: install iwlwifi /bin/false AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 AC-18(4) If Wireless functionality must be disabled, preventing the kernel from loading the kernel module provides an additional safeguard against its activation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install iwlwifi" /etc/modprobe.d/iwlwifi.conf ; then sed -i 's#^install iwlwifi.*#install iwlwifi /bin/false#g' /etc/modprobe.d/iwlwifi.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/iwlwifi.conf echo "install iwlwifi /bin/false" >> /etc/modprobe.d/iwlwifi.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_iwlwifi_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'iwlwifi' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/iwlwifi.conf regexp: install\s+iwlwifi line: install iwlwifi /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_iwlwifi_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20iwlwifi%20/bin/false%0Ablacklist%20iwlwifi%0A mode: 0644 path: /etc/modprobe.d/iwlwifi.conf overwrite: true Disable Kernel mac80211 Module To configure the system to prevent the mac80211 kernel module from being loaded, add the following line to the file /etc/modprobe.d/mac80211.conf: install mac80211 /bin/false AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 AC-18(4) If Wireless functionality must be disabled, preventing the kernel from loading the kernel module provides an additional safeguard against its activation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install mac80211" /etc/modprobe.d/mac80211.conf ; then sed -i 's#^install mac80211.*#install mac80211 /bin/false#g' /etc/modprobe.d/mac80211.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/mac80211.conf echo "install mac80211 /bin/false" >> /etc/modprobe.d/mac80211.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_mac80211_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'mac80211' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/mac80211.conf regexp: install\s+mac80211 line: install mac80211 /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(4) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - kernel_module_mac80211_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20mac80211%20/bin/false%0Ablacklist%20mac80211%0A mode: 0644 path: /etc/modprobe.d/mac80211.conf overwrite: true Disable WiFi or Bluetooth in BIOS Some machines that include built-in wireless support offer the ability to disable the device through the BIOS. This is hardware-specific; consult your hardware manual or explore the BIOS setup during boot. 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 Disabling wireless support in the BIOS prevents easy activation of the wireless interface, generally requiring administrators to reboot the system first. Deactivate Wireless Network Interfaces Deactivating wireless network interfaces should prevent normal usage of the wireless capability. Configure the system to disable all wireless network interfaces with the following command: $ sudo nmcli radio all off 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 3.1.16 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 1315 1319 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 AC-18(a) AC-18(3) CM-7(a) CM-7(b) CM-6(a) MP-7 PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 Req-1.3.3 SRG-OS-000299-GPOS-00117 SRG-OS-000300-GPOS-00118 SRG-OS-000424-GPOS-00188 SRG-OS-000481-GPOS-00481 3.1.2 1.3.3 1.3 The use of wireless networking can introduce many different attack vectors into the organization's network. Common attack vectors such as malicious association and ad hoc networks will allow an attacker to spoof a wireless access point (AP), allowing validated systems to connect to the malicious AP and enabling the attacker to monitor and record network traffic. These malicious APs can also serve to create a man-in-the-middle attack or be used to create a denial of service to valid network resources. # Remediation is applicable only in certain platforms if ( ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then if ! rpm -q --quiet "NetworkManager" ; then dnf install -y "NetworkManager" fi if command -v nmcli >/dev/null 2>&1 && systemctl is-active NetworkManager >/dev/null 2>&1; then nmcli radio all off fi if command -v wicked >/dev/null 2>&1 && systemctl is-active wickedd >/dev/null 2>&1; then if [ -n "$(find /sys/class/net/*/ -type d -name wireless)" ]; then interfaces=$(find /sys/class/net/*/wireless -type d -name wireless | xargs -0 dirname | xargs basename) for iface in $interfaces; do wicked ifdown $iface sed -i 's/STARTMODE=.*/STARTMODE=off/' /etc/sysconfig/network/ifcfg-$iface done fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - PCI-DSS-Req-1.3.3 - PCI-DSSv4-1.3 - PCI-DSSv4-1.3.3 - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - wireless_disable_interfaces - name: Service facts ansible.builtin.service_facts: null when: ( not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - PCI-DSS-Req-1.3.3 - PCI-DSSv4-1.3 - PCI-DSSv4-1.3.3 - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - wireless_disable_interfaces - name: Ensure NetworkManager is installed ansible.builtin.package: name: '{{ item }}' state: present with_items: - NetworkManager when: ( not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - PCI-DSS-Req-1.3.3 - PCI-DSSv4-1.3 - PCI-DSSv4-1.3.3 - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - wireless_disable_interfaces - name: NetworkManager Deactivate Wireless Network Interfaces ansible.builtin.command: nmcli radio wifi off when: - ( not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '''NetworkManager'' in ansible_facts.packages' - ansible_facts.services['NetworkManager.service'].state == 'running' tags: - NIST-800-171-3.1.16 - NIST-800-53-AC-18(3) - NIST-800-53-AC-18(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - PCI-DSS-Req-1.3.3 - PCI-DSSv4-1.3 - PCI-DSSv4-1.3.3 - low_complexity - medium_disruption - medium_severity - no_reboot_needed - unknown_strategy - wireless_disable_interfaces File Permissions and Masks Traditional Unix security relies heavily on file and directory permissions to prevent unauthorized users from reading or modifying files to which they should not have access. Several of the commands in this section search filesystems for files or directories with certain characteristics, and are intended to be run on every local partition on a given system. When the variable PART appears in one of the commands below, it means that the command is intended to be run repeatedly, with the name of each local partition substituted for PART in turn. The following command prints a list of all xfs partitions on the local system, which is the default filesystem for Fedora installations: $ mount -t xfs | awk '{print $3}' For any systems that use a different local filesystem type, modify this command as appropriate. Verify Permissions on Important Files and Directories Permissions for many files on a system must be set restrictively to ensure sensitive information is properly protected. This section discusses important permission restrictions which can be verified to ensure that no harmful discrepancies have arisen. Ensure All World-Writable Directories Are Owned by root User All directories in local partitions which are world-writable should be owned by root. If any world-writable directories are not owned by root, this should be investigated. Following this, the files should be deleted or assigned to root user. SRG-OS-000480-GPOS-00227 SRG-OS-000138-GPOS-00069 R54 Allowing a user account to own a world-writable directory is undesirable because it allows the owner of that directory to remove or replace any files that may be placed in the directory by other users. # At least under containerized env /proc can have files w/o possilibity to # modify even as root. And touching /proc is not good idea anyways. find / -path /proc -prune -o \ -not -fstype afs -not -fstype autofs -not -fstype ceph -not -fstype cifs -not -fstype smb3 \ -not -fstype smbfs -not -fstype sshfs -not -fstype ncpfs -not -fstype ncp -not -fstype nfs \ -not -fstype nfs4 -not -fstype gfs -not -fstype gfs2 -not -fstype glusterfs -not -fstype gpfs \ -not -fstype pvfs2 -not -fstype ocfs2 -not -fstype lustre -not -fstype davfs \ -not -fstype fuse.sshfs -type d -perm -0002 -uid +0 -exec chown root {} \; - name: Ensure All World-Writable Directories Are Owned by root User - Define Excluded (Non-Local) File Systems and Paths ansible.builtin.set_fact: excluded_fstypes: - afs - autofs - ceph - cifs - smb3 - smbfs - sshfs - ncpfs - ncp - nfs - nfs4 - gfs - gfs2 - glusterfs - gpfs - pvfs2 - ocfs2 - lustre - davfs - fuse.sshfs excluded_paths: - dev - proc - run - sys search_paths: [] tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Find Relevant Root Directories Ignoring Pre-Defined Excluded Paths ansible.builtin.find: paths: / file_type: directory excludes: '{{ excluded_paths }}' hidden: true recurse: false register: result_relevant_root_dirs tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Include Relevant Root Directories in a List of Paths to be Searched ansible.builtin.set_fact: search_paths: '{{ search_paths | union([item.path]) }}' loop: '{{ result_relevant_root_dirs.files }}' tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Increment Search Paths List with Local Partitions Mount Points ansible.builtin.set_fact: search_paths: '{{ search_paths | union([item.mount]) }}' loop: '{{ ansible_mounts }}' when: - item.fstype not in excluded_fstypes - item.mount != '/' tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Increment Search Paths List with Local NFS File System Targets ansible.builtin.set_fact: search_paths: '{{ search_paths | union([item.device.split('':'')[1]]) }}' loop: '{{ ansible_mounts }}' when: item.device is search("localhost:") tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Define Rule Specific Facts ansible.builtin.set_fact: world_writable_dirs: [] tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Find All Uncompliant Directories in Local File Systems ansible.builtin.command: cmd: find {{ item }} -xdev -type d -perm -0002 -uid +0 loop: '{{ search_paths }}' changed_when: false register: result_found_dirs tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Create List of World Writable Directories Not Owned by root ansible.builtin.set_fact: world_writable_dirs: '{{ world_writable_dirs | union(item.stdout_lines) | list }}' loop: '{{ result_found_dirs.results }}' when: item is not skipped tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure All World-Writable Directories Are Owned by root User - Ensure root Ownership on Local World Writable Directories ansible.builtin.file: path: '{{ item }}' owner: root loop: '{{ world_writable_dirs }}' tags: - dir_perms_world_writable_root_owned - low_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy Verify that All World-Writable Directories Have Sticky Bits Set When the so-called 'sticky bit' is set on a directory, only the owner of a given file may remove that file from the directory. Without the sticky bit, any user with write access to a directory may remove any file in the directory. Setting the sticky bit prevents users from removing each other's files. In cases where there is no reason for a directory to be world-writable, a better solution is to remove that permission rather than to set the sticky bit. However, if a directory is used by a particular application, consult that application's documentation instead of blindly changing modes. To set the sticky bit on a world-writable directory DIR, run the following command: $ sudo chmod +t DIR This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of directories present on the system. It is not a problem in most cases, but especially systems with a large number of directories can be affected. See https://access.redhat.com/articles/6999111. Please note that there might be cases where the rule remediation cannot fix directory permissions. This can happen for example when running on a system with some immutable parts. These immutable parts cannot be remediated because they are read-only. Example of such directories can be OStree deployments located at /sysroot/ostree/deploy. In such case, it is needed to make modifications to the underlying ostree snapshot and this is out of scope of regular rule remediation. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000138-GPOS-00069 R54 7.1.11 2.2.6 2.2 Failing to set the sticky bit on public directories allows unauthorized users to delete files in the directory structure. The only authorized public directories are those temporary directories supplied with the system, or those designed to be temporary file repositories. The setting is normally reserved for directories used by the system, by users for temporary file storage (such as /tmp), and for directories requiring global read/write access. Ensure All World-Writable Directories Are Owned by a System Account All directories in local partitions which are world-writable should be owned by root or another system account. If any world-writable directories are not owned by a system account, this should be investigated. Following this, the files should be deleted or assigned to an appropriate owner. This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of directories present on the system. It is not a problem in most cases, but especially systems with a large number of directories can be affected. See https://access.redhat.com/articles/6999111. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 Allowing a user account to own a world-writable directory is undesirable because it allows the owner of that directory to remove or replace any files that may be placed in the directory by other users. Ensure All World-Writable Directories Are Group Owned by a System Account All directories in local partitions which are world-writable should be group owned by root or another system account. If any world-writable directories are not group owned by a system account, this should be investigated. Following this, the files should be deleted or assigned to an appropriate group. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 Allowing a user account to group own a world-writable directory is undesirable because it allows the owner of that directory to remove or replace any files that may be placed in the directory by other users. Verify Permissions on System.map Files The System.map files are symbol map files generated during the compilation of the Linux kernel. They contain the mapping between kernel symbols and their corresponding memory addresses. In general, there is no need for non-root users to read these files. To properly set the permissions of /boot/System.map*, run the command: $ sudo chmod 0600 /boot/System.map* R29 The purpose of System.map files is primarily for debugging and profiling the kernel. Unrestricted access to these files might disclose information useful to attackers and malicious software leading to more sophisticated exploitation. find -P /boot/ -maxdepth 1 -perm /u+xs,g+xwrs,o+xwrt -type f -regextype posix-extended -regex '^.*System\.map.*$' -exec chmod u-xs,g-xwrs,o-xwrt {} \; - name: Find /boot/ file(s) ansible.builtin.command: find -P /boot/ -maxdepth 1 -perm /u+xs,g+xwrs,o+xwrt -type f -regextype posix-extended -regex "^.*System\.map.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - file_permissions_systemmap - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Set permissions for /boot/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-xs,g-xwrs,o-xwrt state: file with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - file_permissions_systemmap - low_complexity - low_disruption - low_severity - no_reboot_needed Ensure All SGID Executables Are Authorized The SGID (set group id) bit should be set only on files that were installed via authorized means. A straightforward means of identifying unauthorized SGID files is determine if any were not installed as part of an RPM package, which is cryptographically verified. Investigate the origin of any unpackaged SGID files. This configuration check considers authorized SGID files those which were installed via RPM. It is assumed that when an individual has sudo access to install an RPM and all packages are signed with an organizationally-recognized GPG key, the software should be considered an approved package on the system. Any SGID file not deployed through an RPM will be flagged for further review. This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of files present on the system. It is not a problem in most cases, but especially systems with a large number of files can be affected. See https://access.redhat.com/articles/6999111. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 R56 Executable files with the SGID permission run with the privileges of the owner of the file. SGID files of uncertain provenance could allow for unprivileged users to elevate privileges. The presence of these files should be strictly controlled on the system. Ensure All SUID Executables Are Authorized The SUID (set user id) bit should be set only on files that were installed via authorized means. A straightforward means of identifying unauthorized SUID files is determine if any were not installed as part of an RPM package, which is cryptographically verified. Investigate the origin of any unpackaged SUID files. This configuration check considers authorized SUID files those which were installed via RPM. It is assumed that when an individual has sudo access to install an RPM and all packages are signed with an organizationally-recognized GPG key, the software should be considered an approved package on the system. Any SUID file not deployed through an RPM will be flagged for further review. This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of files present on the system. It is not a problem in most cases, but especially systems with a large number of files can be affected. See https://access.redhat.com/articles/6999111. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 R56 Executable files with the SUID permission run with the privileges of the owner of the file. SUID files of uncertain provenance could allow for unprivileged users to elevate privileges. The presence of these files should be strictly controlled on the system. Ensure No World-Writable Files Exist It is generally a good idea to remove global (other) write access to a file when it is discovered. However, check with documentation for specific applications before making changes. Also, monitor for recurring world-writable files, as these may be symptoms of a misconfigured application or user account. Finally, this applies to real files and not virtual files that are a part of pseudo file systems such as sysfs or procfs. This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of files present on the system. It is not a problem in most cases, but especially systems with a large number of files can be affected. See https://access.redhat.com/articles/6999111. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 R54 7.1.11 2.2.6 2.2 Data in world-writable files can be modified by any user on the system. In almost all circumstances, files can be configured using a combination of user and group permissions to support whatever legitimate access is needed without the risk caused by world-writable files. FILTER_NODEV=$(awk '/nodev/ { print $2 }' /proc/filesystems | paste -sd,) # Do not consider /sysroot partition because it contains only the physical # read-only root on bootable containers. PARTITIONS=$(findmnt -n -l -k -it $FILTER_NODEV | awk '{ print $1 }' | grep -v "/sysroot") for PARTITION in $PARTITIONS; do find "${PARTITION}" -xdev -type f -perm -002 -exec chmod o-w {} \; 2>/dev/null done # Ensure /tmp is also fixed when tmpfs is used. if grep "^tmpfs /tmp" /proc/mounts; then find /tmp -xdev -type f -perm -002 -exec chmod o-w {} \; 2>/dev/null fi Ensure All Files Are Owned by a Group If any file is not group-owned by a valid defined group, the cause of the lack of group-ownership must be investigated. Following this, those files should be deleted or assigned to an appropriate group. The groups need to be defined in /etc/group or in /usr/lib/group if nss-altfiles are configured to be used in /etc/nsswitch.conf. Locate the mount points related to local devices by the following command: $ findmnt -n -l -k -it $(awk '/nodev/ { print $2 }' /proc/filesystems | paste -sd,) For all mount points listed by the previous command, it is necessary to search for files which do not belong to a valid group using the following command: $ sudo find MOUNTPOINT -xdev -nogroup 2>/dev/null This rule only considers local groups as valid groups. If you have your groups defined outside /etc/group or /usr/lib/group, the rule won't consider those. This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of files present on the system. It is not a problem in most cases, but especially systems with a large number of files can be affected. See https://access.redhat.com/articles/6999111. 1 11 12 13 14 15 16 18 3 5 APO01.06 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.02 DSS06.03 DSS06.06 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.18.1.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.DS-5 PR.PT-3 SRG-OS-000480-GPOS-00227 R53 7.1.12 2.2.6 2.2 Unowned files do not directly imply a security problem, but they are generally a sign that something is amiss. They may be caused by an intruder, by incorrect software installation or draft software removal, or by failure to remove all files belonging to a deleted account, or other similar cases. The files should be repaired so they will not cause problems when accounts are created in the future, and the cause should be discovered and addressed. Ensure All Files Are Owned by a User If any files are not owned by a user, then the cause of their lack of ownership should be investigated. Following this, the files should be deleted or assigned to an appropriate user. Locate the mount points related to local devices by the following command: $ findmnt -n -l -k -it $(awk '/nodev/ { print $2 }' /proc/filesystems | paste -sd,) For all mount points listed by the previous command, it is necessary to search for files which do not belong to a valid user using the following command: $ sudo find MOUNTPOINT -xdev -nouser 2>/dev/null For this rule to evaluate centralized user accounts, getent must be working properly so that running the command getent passwd returns a list of all users in your organization. If using the System Security Services Daemon (SSSD), enumerate = true must be configured in your organization's domain to return a complete list of users This rule can take a long time to perform the check and might consume a considerable amount of resources depending on the number of files present on the system. It is not a problem in most cases, but especially systems with a large number of files can be affected. See https://access.redhat.com/articles/6999111. 11 12 13 14 15 16 18 3 5 9 APO01.06 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.03 DSS06.06 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 5.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.AC-6 PR.DS-5 PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 R53 7.1.12 2.2.6 2.2 Unowned files do not directly imply a security problem, but they are generally a sign that something is amiss. They may be caused by an intruder, by incorrect software installation or draft software removal, or by failure to remove all files belonging to a deleted account, or other similar cases. The files should be repaired so they will not cause problems when accounts are created in the future, and the cause should be discovered and addressed. Enable Kernel Parameter to Enforce DAC on FIFOs To set the runtime status of the fs.protected_fifos kernel parameter, run the following command: $ sudo sysctl -w fs.protected_fifos=2 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: fs.protected_fifos = 2 CM-6(a) AC-6(1) R14 This parameter is available since Linux Kernel 4.19 and allows to prohibit opening FIFOs that are not owned by the user in world and group writeable sticky directories. It avoids unintentional writes to an attacker-controlled FIFO where a program expects to create the regular file. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of fs.protected_fifos from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*fs.protected_fifos.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "fs.protected_fifos" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for fs.protected_fifos # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w fs.protected_fifos="2" fi # # If fs.protected_fifos present in /etc/sysctl.conf, change value to "2" # else, add "fs.protected_fifos = 2" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^fs.protected_fifos") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "2" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^fs.protected_fifos\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^fs.protected_fifos\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_fifos - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*fs.protected_fifos.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_fifos - name: Comment out any occurrences of fs.protected_fifos from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*fs.protected_fifos replace: '#fs.protected_fifos' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_fifos - name: Ensure sysctl fs.protected_fifos is set to 2 ansible.posix.sysctl: name: fs.protected_fifos value: '2' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_fifos Enable Kernel Parameter to Enforce DAC on Hardlinks To set the runtime status of the fs.protected_hardlinks kernel parameter, run the following command: $ sudo sysctl -w fs.protected_hardlinks=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: fs.protected_hardlinks = 1 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) SRG-OS-000312-GPOS-00122 SRG-OS-000312-GPOS-00123 SRG-OS-000324-GPOS-00125 R14 By enabling this kernel parameter, users can no longer create soft or hard links to files which they do not own. Disallowing such hardlinks mitigate vulnerabilities based on insecure file system accessed by privileged programs, avoiding an exploitation vector exploiting unsafe use of open() or creat(). # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of fs.protected_hardlinks from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*fs.protected_hardlinks.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "fs.protected_hardlinks" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for fs.protected_hardlinks # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w fs.protected_hardlinks="1" fi # # If fs.protected_hardlinks present in /etc/sysctl.conf, change value to "1" # else, add "fs.protected_hardlinks = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^fs.protected_hardlinks") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^fs.protected_hardlinks\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^fs.protected_hardlinks\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_hardlinks - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*fs.protected_hardlinks.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_hardlinks - name: Comment out any occurrences of fs.protected_hardlinks from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*fs.protected_hardlinks replace: '#fs.protected_hardlinks' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_hardlinks - name: Ensure sysctl fs.protected_hardlinks is set to 1 ansible.posix.sysctl: name: fs.protected_hardlinks value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_hardlinks --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,fs.protected_hardlinks%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_fs_protected_hardlinks.conf overwrite: true Enable Kernel Parameter to Enforce DAC on Regular files To set the runtime status of the fs.protected_regular kernel parameter, run the following command: $ sudo sysctl -w fs.protected_regular=2 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: fs.protected_regular = 2 CM-6(a) AC-6(1) R14 This parameter is available since Linux Kernel 4.19 and allows to prohibit opening "regular" files that are not owned by the user in world and group writeable sticky directories. It avoids writes to an attacker-controlled regular file, for example, when a program expects to create the regular file. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of fs.protected_regular from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*fs.protected_regular.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "fs.protected_regular" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for fs.protected_regular # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w fs.protected_regular="2" fi # # If fs.protected_regular present in /etc/sysctl.conf, change value to "2" # else, add "fs.protected_regular = 2" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^fs.protected_regular") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "2" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^fs.protected_regular\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^fs.protected_regular\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_regular - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*fs.protected_regular.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_regular - name: Comment out any occurrences of fs.protected_regular from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*fs.protected_regular replace: '#fs.protected_regular' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_regular - name: Ensure sysctl fs.protected_regular is set to 2 ansible.posix.sysctl: name: fs.protected_regular value: '2' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_regular Enable Kernel Parameter to Enforce DAC on Symlinks To set the runtime status of the fs.protected_symlinks kernel parameter, run the following command: $ sudo sysctl -w fs.protected_symlinks=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: fs.protected_symlinks = 1 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) SRG-OS-000312-GPOS-00122 SRG-OS-000312-GPOS-00123 SRG-OS-000324-GPOS-00125 R14 By enabling this kernel parameter, symbolic links are permitted to be followed only when outside a sticky world-writable directory, or when the UID of the link and follower match, or when the directory owner matches the symlink's owner. Disallowing such symlinks helps mitigate vulnerabilities based on insecure file system accessed by privileged programs, avoiding an exploitation vector exploiting unsafe use of open() or creat(). # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of fs.protected_symlinks from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*fs.protected_symlinks.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "fs.protected_symlinks" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for fs.protected_symlinks # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w fs.protected_symlinks="1" fi # # If fs.protected_symlinks present in /etc/sysctl.conf, change value to "1" # else, add "fs.protected_symlinks = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^fs.protected_symlinks") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^fs.protected_symlinks\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^fs.protected_symlinks\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_symlinks - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*fs.protected_symlinks.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_symlinks - name: Comment out any occurrences of fs.protected_symlinks from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*fs.protected_symlinks replace: '#fs.protected_symlinks' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_symlinks - name: Ensure sysctl fs.protected_symlinks is set to 1 ansible.posix.sysctl: name: fs.protected_symlinks value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_protected_symlinks --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,fs.protected_symlinks%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_fs_protected_symlinks.conf overwrite: true Verify Permissions on Files with Local Account Information and Credentials The default restrictive permissions for files which act as important security databases such as passwd, shadow, group, and gshadow files must be maintained. Many utilities need read access to the passwd file in order to function properly, but read access to the shadow file allows malicious attacks against system passwords, and should never be enabled. Verify Permissions and Ownership of Old Passwords File To properly set the owner of /etc/security/opasswd, run the command: $ sudo chown root /etc/security/opasswd To properly set the group owner of /etc/security/opasswd, run the command: $ sudo chgrp root /etc/security/opasswd To properly set the permissions of /etc/security/opasswd, run the command: $ sudo chmod 0600 /etc/security/opasswd SRG-OS-000077-GPOS-00045 7.1.10 The /etc/security/opasswd file stores old passwords to prevent password reuse. Protection of this file is critical for system security. Verify Group Who Owns Backup group File To properly set the group owner of /etc/group-, run the command: $ sudo chgrp root /etc/group- AC-6 (1) Req-8.7 SRG-OS-000480-GPOS-00227 7.1.4 2.2.6 2.2 The /etc/group- file is a backup file of /etc/group, and as such, it contains information regarding groups that are configured on the system. Protection of this file is important for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/group-" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/group- fi fi - name: Set the file_groupowner_backup_etc_group_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_backup_etc_group_newgroup: '0' tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/group- ansible.builtin.stat: path: /etc/group- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/group- ansible.builtin.file: path: /etc/group- follow: false group: '{{ file_groupowner_backup_etc_group_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns Backup gshadow File To properly set the group owner of /etc/gshadow-, run the command: $ sudo chgrp root /etc/gshadow- AC-6 (1) Req-8.7 SRG-OS-000480-GPOS-00227 7.1.8 The /etc/gshadow- file is a backup of /etc/gshadow, and as such, it contains group password hashes. Protection of this file is critical for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/gshadow-" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/gshadow- fi fi - name: Set the file_groupowner_backup_etc_gshadow_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_backup_etc_gshadow_newgroup: '0' tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - configure_strategy - file_groupowner_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/gshadow- ansible.builtin.stat: path: /etc/gshadow- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - configure_strategy - file_groupowner_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/gshadow- ansible.builtin.file: path: /etc/gshadow- follow: false group: '{{ file_groupowner_backup_etc_gshadow_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - configure_strategy - file_groupowner_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns Backup passwd File To properly set the group owner of /etc/passwd-, run the command: $ sudo chgrp root /etc/passwd- AC-6 (1) Req-8.7 SRG-OS-000480-GPOS-00227 7.1.2 2.2.6 2.2 The /etc/passwd- file is a backup file of /etc/passwd, and as such, it contains information about the users that are configured on the system. Protection of this file is critical for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/passwd-" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/passwd- fi fi - name: Set the file_groupowner_backup_etc_passwd_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_backup_etc_passwd_newgroup: '0' tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/passwd- ansible.builtin.stat: path: /etc/passwd- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/passwd- ansible.builtin.file: path: /etc/passwd- follow: false group: '{{ file_groupowner_backup_etc_passwd_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns Backup shadow File To properly set the group owner of /etc/shadow-, run the command: $ sudo chgrp root /etc/shadow- Req-8.7 SRG-OS-000480-GPOS-00227 7.1.6 2.2.6 2.2 The /etc/shadow- file is a backup file of /etc/shadow, and as such, it contains the list of local system accounts and password hashes. Protection of this file is critical for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/shadow-" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/shadow- fi fi - name: Set the file_groupowner_backup_etc_shadow_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_backup_etc_shadow_newgroup: '0' tags: - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/shadow- ansible.builtin.stat: path: /etc/shadow- register: file_exists tags: - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/shadow- ansible.builtin.file: path: /etc/shadow- follow: false group: '{{ file_groupowner_backup_etc_shadow_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSS-Req-8.7 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns group File To properly set the group owner of /etc/group, run the command: $ sudo chgrp root /etc/group 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.3 2.2.6 2.2 The /etc/group file contains information regarding groups that are configured on the system. Protection of this file is important for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/group" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/group fi fi - name: Set the file_groupowner_etc_group_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_group_newgroup: '0' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/group ansible.builtin.stat: path: /etc/group register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/group ansible.builtin.file: path: /etc/group follow: false group: '{{ file_groupowner_etc_group_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns gshadow File To properly set the group owner of /etc/gshadow, run the command: $ sudo chgrp root /etc/gshadow 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 R50 7.1.7 The /etc/gshadow file contains group password hashes. Protection of this file is critical for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/gshadow" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/gshadow fi fi - name: Set the file_groupowner_etc_gshadow_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_gshadow_newgroup: '0' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_groupowner_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/gshadow ansible.builtin.stat: path: /etc/gshadow register: file_exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_groupowner_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/gshadow ansible.builtin.file: path: /etc/gshadow follow: false group: '{{ file_groupowner_etc_gshadow_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_groupowner_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns passwd File To properly set the group owner of /etc/passwd, run the command: $ sudo chgrp root /etc/passwd 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.1 2.2.6 2.2 The /etc/passwd file contains information about the users that are configured on the system. Protection of this file is critical for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/passwd" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/passwd fi fi - name: Set the file_groupowner_etc_passwd_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_passwd_newgroup: '0' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/passwd ansible.builtin.stat: path: /etc/passwd register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/passwd ansible.builtin.file: path: /etc/passwd follow: false group: '{{ file_groupowner_etc_passwd_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns shadow File To properly set the group owner of /etc/shadow, run the command: $ sudo chgrp root /etc/shadow 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.5 2.2.6 2.2 The /etc/shadow file stores password hashes. Protection of this file is critical for system security. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/shadow" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/shadow fi fi - name: Set the file_groupowner_etc_shadow_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_shadow_newgroup: '0' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/shadow ansible.builtin.stat: path: /etc/shadow register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/shadow ansible.builtin.file: path: /etc/shadow follow: false group: '{{ file_groupowner_etc_shadow_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns /etc/shells File To properly set the group owner of /etc/shells, run the command: $ sudo chgrp root /etc/shells AC-3 MP-2 R50 7.1.9 The /etc/shells file contains the list of full pathnames to shells on the system. Since this file is used by many system programs this file should be protected. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/shells" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/shells fi fi - name: Set the file_groupowner_etc_shells_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_etc_shells_newgroup: '0' tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_groupowner_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/shells ansible.builtin.stat: path: /etc/shells register: file_exists tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_groupowner_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/shells ansible.builtin.file: path: /etc/shells follow: false group: '{{ file_groupowner_etc_shells_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_groupowner_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns Backup group File To properly set the owner of /etc/group-, run the command: $ sudo chown root /etc/group- AC-6 (1) Req-8.7.c SRG-OS-000480-GPOS-00227 7.1.4 2.2.6 2.2 The /etc/group- file is a backup file of /etc/group, and as such, it contains information regarding groups that are configured on the system. Protection of this file is important for system security. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/group-" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/group- fi fi - name: Set the file_owner_backup_etc_group_newown variable if represented by uid ansible.builtin.set_fact: file_owner_backup_etc_group_newown: '0' tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/group- ansible.builtin.stat: path: /etc/group- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/group- ansible.builtin.file: path: /etc/group- follow: false owner: '{{ file_owner_backup_etc_group_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns Backup gshadow File To properly set the owner of /etc/gshadow-, run the command: $ sudo chown root /etc/gshadow- AC-6 (1) Req-8.7 SRG-OS-000480-GPOS-00227 7.1.8 The /etc/gshadow- file is a backup of /etc/gshadow, and as such, it contains group password hashes. Protection of this file is critical for system security. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/gshadow-" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/gshadow- fi fi - name: Set the file_owner_backup_etc_gshadow_newown variable if represented by uid ansible.builtin.set_fact: file_owner_backup_etc_gshadow_newown: '0' tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - configure_strategy - file_owner_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/gshadow- ansible.builtin.stat: path: /etc/gshadow- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - configure_strategy - file_owner_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/gshadow- ansible.builtin.file: path: /etc/gshadow- follow: false owner: '{{ file_owner_backup_etc_gshadow_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7 - configure_strategy - file_owner_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns Backup passwd File To properly set the owner of /etc/passwd-, run the command: $ sudo chown root /etc/passwd- AC-6 (1) Req-8.7.c SRG-OS-000480-GPOS-00227 7.1.2 2.2.6 2.2 The /etc/passwd- file is a backup file of /etc/passwd, and as such, it contains information about the users that are configured on the system. Protection of this file is critical for system security. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/passwd-" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/passwd- fi fi - name: Set the file_owner_backup_etc_passwd_newown variable if represented by uid ansible.builtin.set_fact: file_owner_backup_etc_passwd_newown: '0' tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/passwd- ansible.builtin.stat: path: /etc/passwd- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/passwd- ansible.builtin.file: path: /etc/passwd- follow: false owner: '{{ file_owner_backup_etc_passwd_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns Backup shadow File To properly set the owner of /etc/shadow-, run the command: $ sudo chown root /etc/shadow- AC-6 (1) Req-8.7.c SRG-OS-000480-GPOS-00227 7.1.6 2.2.6 2.2 The /etc/shadow- file is a backup file of /etc/shadow, and as such, it contains the list of local system accounts and password hashes. Protection of this file is critical for system security. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/shadow-" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/shadow- fi fi - name: Set the file_owner_backup_etc_shadow_newown variable if represented by uid ansible.builtin.set_fact: file_owner_backup_etc_shadow_newown: '0' tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/shadow- ansible.builtin.stat: path: /etc/shadow- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/shadow- ansible.builtin.file: path: /etc/shadow- follow: false owner: '{{ file_owner_backup_etc_shadow_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns group File To properly set the owner of /etc/group, run the command: $ sudo chown root /etc/group 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.3 2.2.6 2.2 The /etc/group file contains information regarding groups that are configured on the system. Protection of this file is important for system security. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/group" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/group fi fi - name: Set the file_owner_etc_group_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_group_newown: '0' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/group ansible.builtin.stat: path: /etc/group register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/group ansible.builtin.file: path: /etc/group follow: false owner: '{{ file_owner_etc_group_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns gshadow File To properly set the owner of /etc/gshadow, run the command: $ sudo chown root /etc/gshadow 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 R50 7.1.7 The /etc/gshadow file contains group password hashes. Protection of this file is critical for system security. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/gshadow" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/gshadow fi fi - name: Set the file_owner_etc_gshadow_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_gshadow_newown: '0' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_owner_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/gshadow ansible.builtin.stat: path: /etc/gshadow register: file_exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_owner_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/gshadow ansible.builtin.file: path: /etc/gshadow follow: false owner: '{{ file_owner_etc_gshadow_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_owner_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns passwd File To properly set the owner of /etc/passwd, run the command: $ sudo chown root /etc/passwd 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.1 2.2.6 2.2 The /etc/passwd file contains information about the users that are configured on the system. Protection of this file is critical for system security. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/passwd" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/passwd fi fi - name: Set the file_owner_etc_passwd_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_passwd_newown: '0' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/passwd ansible.builtin.stat: path: /etc/passwd register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/passwd ansible.builtin.file: path: /etc/passwd follow: false owner: '{{ file_owner_etc_passwd_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns shadow File To properly set the owner of /etc/shadow, run the command: $ sudo chown root /etc/shadow 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.5 2.2.6 2.2 The /etc/shadow file contains the list of local system accounts and stores password hashes. Protection of this file is critical for system security. Failure to give ownership of this file to root provides the designated owner with access to sensitive information which could weaken the system security posture. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/shadow" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/shadow fi fi - name: Set the file_owner_etc_shadow_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_shadow_newown: '0' tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/shadow ansible.builtin.stat: path: /etc/shadow register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/shadow ansible.builtin.file: path: /etc/shadow follow: false owner: '{{ file_owner_etc_shadow_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Who Owns /etc/shells File To properly set the owner of /etc/shells, run the command: $ sudo chown root /etc/shells AC-3 MP-2 R50 7.1.9 The /etc/shells file contains the list of full pathnames to shells on the system. Since this file is used by many system programs this file should be protected. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/shells" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/shells fi fi - name: Set the file_owner_etc_shells_newown variable if represented by uid ansible.builtin.set_fact: file_owner_etc_shells_newown: '0' tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_owner_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/shells ansible.builtin.stat: path: /etc/shells register: file_exists tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_owner_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/shells ansible.builtin.file: path: /etc/shells follow: false owner: '{{ file_owner_etc_shells_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_owner_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on Backup group File To properly set the permissions of /etc/group-, run the command: $ sudo chmod 0644 /etc/group- AC-6 (1) Req-8.7.c SRG-OS-000480-GPOS-00227 7.1.4 2.2.6 2.2 The /etc/group- file is a backup file of /etc/group, and as such, it contains information regarding groups that are configured on the system. Protection of this file is important for system security. chmod u-xs,g-xws,o-xwt /etc/group- - name: Test for existence /etc/group- ansible.builtin.stat: path: /etc/group- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/group- ansible.builtin.file: path: /etc/group- mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_backup_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on Backup gshadow File To properly set the permissions of /etc/gshadow-, run the command: $ sudo chmod 0000 /etc/gshadow- AC-6 (1) SRG-OS-000480-GPOS-00227 7.1.8 The /etc/gshadow- file is a backup of /etc/gshadow, and as such, it contains group password hashes. Protection of this file is critical for system security. chmod u-xwrs,g-xwrs,o-xwrt /etc/gshadow- - name: Test for existence /etc/gshadow- ansible.builtin.stat: path: /etc/gshadow- register: file_exists tags: - NIST-800-53-AC-6 (1) - configure_strategy - file_permissions_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xwrs,g-xwrs,o-xwrt on /etc/gshadow- ansible.builtin.file: path: /etc/gshadow- mode: u-xwrs,g-xwrs,o-xwrt when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - configure_strategy - file_permissions_backup_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on Backup passwd File To properly set the permissions of /etc/passwd-, run the command: $ sudo chmod 0644 /etc/passwd- AC-6 (1) Req-8.7.c SRG-OS-000480-GPOS-00227 7.1.2 2.2.6 2.2 The /etc/passwd- file is a backup file of /etc/passwd, and as such, it contains information about the users that are configured on the system. Protection of this file is critical for system security. chmod u-xs,g-xws,o-xwt /etc/passwd- - name: Test for existence /etc/passwd- ansible.builtin.stat: path: /etc/passwd- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/passwd- ansible.builtin.file: path: /etc/passwd- mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_backup_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on Backup shadow File To properly set the permissions of /etc/shadow-, run the command: $ sudo chmod 0000 /etc/shadow- AC-6 (1) Req-8.7.c SRG-OS-000480-GPOS-00227 7.1.6 2.2.6 2.2 The /etc/shadow- file is a backup file of /etc/shadow, and as such, it contains the list of local system accounts and password hashes. Protection of this file is critical for system security. chmod u-xwrs,g-xwrs,o-xwrt /etc/shadow- - name: Test for existence /etc/shadow- ansible.builtin.stat: path: /etc/shadow- register: file_exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xwrs,g-xwrs,o-xwrt on /etc/shadow- ansible.builtin.file: path: /etc/shadow- mode: u-xwrs,g-xwrs,o-xwrt when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6 (1) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_backup_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on group File To properly set the permissions of /etc/group, run the command: $ sudo chmod 0644 /etc/group 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.3 2.2.6 2.2 The /etc/group file contains information regarding groups that are configured on the system. Protection of this file is important for system security. chmod u-xs,g-xws,o-xwt /etc/group - name: Test for existence /etc/group ansible.builtin.stat: path: /etc/group register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/group ansible.builtin.file: path: /etc/group mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_etc_group - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on gshadow File To properly set the permissions of /etc/gshadow, run the command: $ sudo chmod 0000 /etc/gshadow 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 R50 7.1.7 The /etc/gshadow file contains group password hashes. Protection of this file is critical for system security. chmod u-xwrs,g-xwrs,o-xwrt /etc/gshadow - name: Test for existence /etc/gshadow ansible.builtin.stat: path: /etc/gshadow register: file_exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xwrs,g-xwrs,o-xwrt on /etc/gshadow ansible.builtin.file: path: /etc/gshadow mode: u-xwrs,g-xwrs,o-xwrt when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_etc_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on passwd File To properly set the permissions of /etc/passwd, run the command: $ sudo chmod 0644 /etc/passwd 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.1 2.2.6 2.2 If the /etc/passwd file is writable by a group-owner or the world the risk of its compromise is increased. The file contains the list of accounts on the system and associated information, and protection of this file is critical for system security. chmod u-xs,g-xws,o-xwt /etc/passwd - name: Test for existence /etc/passwd ansible.builtin.stat: path: /etc/passwd register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/passwd ansible.builtin.file: path: /etc/passwd mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_etc_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on shadow File To properly set the permissions of /etc/shadow, run the command: $ sudo chmod 0000 /etc/shadow 12 13 14 15 16 18 3 5 5.5.2.2 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-8.7.c SRG-OS-000480-GPOS-00227 R50 7.1.5 2.2.6 2.2 The /etc/shadow file contains the list of local system accounts and stores password hashes. Protection of this file is critical for system security. Failure to give ownership of this file to root provides the designated owner with access to sensitive information which could weaken the system security posture. chmod u-xwrs,g-xwrs,o-xwrt /etc/shadow - name: Test for existence /etc/shadow ansible.builtin.stat: path: /etc/shadow register: file_exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xwrs,g-xwrs,o-xwrt on /etc/shadow ansible.builtin.file: path: /etc/shadow mode: u-xwrs,g-xwrs,o-xwrt when: file_exists.stat is defined and file_exists.stat.exists tags: - CJIS-5.5.2.2 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-8.7.c - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_etc_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /etc/shells File To properly set the permissions of /etc/shells, run the command: $ sudo chmod 0644 /etc/shells AC-3 MP-2 R50 7.1.9 The /etc/shells file contains the list of full pathnames to shells on the system. Since this file is used by many system programs this file should be protected. chmod u-xs,g-xws,o-xwt /etc/shells - name: Test for existence /etc/shells ansible.builtin.stat: path: /etc/shells register: file_exists tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_permissions_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwt on /etc/shells ansible.builtin.file: path: /etc/shells mode: u-xs,g-xws,o-xwt when: file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-3 - NIST-800-53-MP-2 - configure_strategy - file_permissions_etc_shells - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on Files within /var/log Directory The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel. Verify Group Who Owns /var/log Directory To properly set the group owner of /var/log, run the command: $ sudo chgrp root /var/log SRG-OS-000206-GPOS-00084 SRG-APP-000118-CTR-000240 The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /var/log/ -maxdepth 0 -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; fi - name: Set the file_groupowner_var_log_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_var_log_newgroup: '0' tags: - configure_strategy - file_groupowner_var_log - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /var/log/ ansible.builtin.file: path: /var/log/ follow: false state: directory group: '{{ file_groupowner_var_log_newgroup }}' tags: - configure_strategy - file_groupowner_var_log - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns /var/log/messages File To properly set the group owner of /var/log/messages, run the command: $ sudo chgrp root /var/log/messages SRG-OS-000206-GPOS-00084 The /var/log/messages file contains logs of error messages in the system and should only be accessed by authorized personnel. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/var/log/messages" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /var/log/messages fi fi - name: Set the file_groupowner_var_log_messages_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_var_log_messages_newgroup: '0' tags: - configure_strategy - file_groupowner_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /var/log/messages ansible.builtin.stat: path: /var/log/messages register: file_exists tags: - configure_strategy - file_groupowner_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /var/log/messages ansible.builtin.file: path: /var/log/messages follow: false group: '{{ file_groupowner_var_log_messages_newgroup }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupowner_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns /var/log/syslog File To properly set the group owner of /var/log/syslog, run the command: $ sudo chgrp adm /var/log/syslog SRG-OS-000206-GPOS-00084 The /var/log/syslog file contains logs of error messages in the system and should only be accessed by authorized personnel. # Remediation is applicable only in certain platforms if rpm --quiet -q rsyslog; then newgroup="" if getent group "4" >/dev/null 2>&1; then newgroup="4" fi if [[ -z "${newgroup}" ]]; then >&2 echo "4 is not a defined group on the system" else if ! stat -c "%g %G" "/var/log/syslog" | grep -E -w -q "4"; then chgrp --no-dereference "$newgroup" /var/log/syslog fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_groupowner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_var_log_syslog_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_var_log_syslog_newgroup: '4' when: '"rsyslog" in ansible_facts.packages' tags: - configure_strategy - file_groupowner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /var/log/syslog ansible.builtin.stat: path: /var/log/syslog register: file_exists when: '"rsyslog" in ansible_facts.packages' tags: - configure_strategy - file_groupowner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /var/log/syslog ansible.builtin.file: path: /var/log/syslog follow: false group: '{{ file_groupowner_var_log_syslog_newgroup }}' when: - '"rsyslog" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupowner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns /var/log Directory To properly set the owner of /var/log, run the command: $ sudo chown root /var/log SRG-OS-000206-GPOS-00084 SRG-APP-000118-CTR-000240 The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /var/log/ -maxdepth 0 -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi - name: Set the file_owner_var_log_newown variable if represented by uid ansible.builtin.set_fact: file_owner_var_log_newown: '0' tags: - configure_strategy - file_owner_var_log - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /var/log/ ansible.builtin.file: path: /var/log/ follow: false state: directory owner: '{{ file_owner_var_log_newown }}' tags: - configure_strategy - file_owner_var_log - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns /var/log/messages File To properly set the owner of /var/log/messages, run the command: $ sudo chown root /var/log/messages SRG-OS-000206-GPOS-00084 The /var/log/messages file contains logs of error messages in the system and should only be accessed by authorized personnel. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/var/log/messages" | grep -E -w -q "0"; then chown --no-dereference "$newown" /var/log/messages fi fi - name: Set the file_owner_var_log_messages_newown variable if represented by uid ansible.builtin.set_fact: file_owner_var_log_messages_newown: '0' tags: - configure_strategy - file_owner_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /var/log/messages ansible.builtin.stat: path: /var/log/messages register: file_exists tags: - configure_strategy - file_owner_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /var/log/messages ansible.builtin.file: path: /var/log/messages follow: false owner: '{{ file_owner_var_log_messages_newown }}' when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_owner_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns /var/log/syslog File To properly set the owner of /var/log/syslog, run the command: $ sudo chown syslog /var/log/syslog SRG-OS-000206-GPOS-00084 The /var/log/syslog file contains logs of error messages in the system and should only be accessed by authorized personnel. # Remediation is applicable only in certain platforms if rpm --quiet -q rsyslog; then newown="" if id "syslog" >/dev/null 2>&1; then newown="syslog" fi if [[ -z "$newown" ]]; then >&2 echo "syslog is not a defined user on the system" else if ! stat -c "%u %U" "/var/log/syslog" | grep -E -w -q "syslog"; then chown --no-dereference "$newown" /var/log/syslog fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_owner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Check that the syslog user is defined ansible.builtin.getent: database: passwd key: syslog ignore_errors: true when: '"rsyslog" in ansible_facts.packages' tags: - configure_strategy - file_owner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_var_log_syslog_newown variable if syslog found ansible.builtin.set_fact: file_owner_var_log_syslog_newown: syslog when: - '"rsyslog" in ansible_facts.packages' - ansible_facts.getent_passwd["syslog"] is defined tags: - configure_strategy - file_owner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /var/log/syslog ansible.builtin.stat: path: /var/log/syslog register: file_exists when: '"rsyslog" in ansible_facts.packages' tags: - configure_strategy - file_owner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /var/log/syslog ansible.builtin.file: path: /var/log/syslog follow: false owner: '{{ file_owner_var_log_syslog_newown }}' when: - '"rsyslog" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_owner_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /var/log Directory To properly set the permissions of /var/log, run the command: $ sudo chmod 0755 /var/log SRG-OS-000206-GPOS-00084 SRG-APP-000118-CTR-000240 The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel. find -H /var/log/ -maxdepth 0 -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \; - name: Find /var/log/ file(s) ansible.builtin.command: 'find -P /var/log/ -maxdepth 0 -perm /u+s,g+ws,o+wt -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - file_permissions_var_log - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /var/log/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-ws,o-wt state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - file_permissions_var_log - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /var/log/messages File To properly set the permissions of /var/log/messages, run the command: $ sudo chmod 0600 /var/log/messages SRG-OS-000206-GPOS-00084 The /var/log/messages file contains logs of error messages in the system and should only be accessed by authorized personnel. chmod u-xs,g-xwrs,o-xwrt /var/log/messages - name: Test for existence /var/log/messages ansible.builtin.stat: path: /var/log/messages register: file_exists tags: - configure_strategy - file_permissions_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xwrs,o-xwrt on /var/log/messages ansible.builtin.file: path: /var/log/messages mode: u-xs,g-xwrs,o-xwrt when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_var_log_messages - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /var/log/syslog File To properly set the permissions of /var/log/syslog, run the command: $ sudo chmod 0640 /var/log/syslog SRG-OS-000206-GPOS-00084 The /var/log/syslog file contains logs of error messages in the system and should only be accessed by authorized personnel. chmod u-xs,g-xws,o-xwrt /var/log/syslog - name: Test for existence /var/log/syslog ansible.builtin.stat: path: /var/log/syslog register: file_exists tags: - configure_strategy - file_permissions_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwrt on /var/log/syslog ansible.builtin.file: path: /var/log/syslog mode: u-xs,g-xws,o-xwrt when: file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_var_log_syslog - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify File Permissions Within Some Important Directories Some directories contain files whose confidentiality or integrity is notably important and may also be susceptible to misconfiguration over time, particularly if unpackaged software is installed. As such, an argument exists to verify that files' permissions within these directories remain configured correctly and restrictively. Verify that Shared Library Directories Have Root Group Ownership System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default: /lib /lib64 /usr/lib /usr/lib64 Kernel modules, which can be added to the kernel during runtime, are also stored in /lib/modules. All files in these directories should be group-owned by the root user. If the directories, is found to be owned by a user other than root correct its ownership with the following command: $ sudo chgrp root DIR CM-5(6) CM-5(6).1 SRG-OS-000259-GPOS-00100 Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Proper ownership of library directories is necessary to protect the integrity of the system. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /lib/ -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; find -P /lib64/ -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; find -P /usr/lib/ -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; find -P /usr/lib64/ -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; fi - name: Set the dir_group_ownership_library_dirs_newgroup variable if represented by gid ansible.builtin.set_fact: dir_group_ownership_library_dirs_newgroup: '0' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_group_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /lib/ recursively ansible.builtin.file: path: /lib/ follow: false state: directory recurse: true group: '{{ dir_group_ownership_library_dirs_newgroup }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_group_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /lib64/ recursively ansible.builtin.file: path: /lib64/ follow: false state: directory recurse: true group: '{{ dir_group_ownership_library_dirs_newgroup }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_group_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /usr/lib/ recursively ansible.builtin.file: path: /usr/lib/ follow: false state: directory recurse: true group: '{{ dir_group_ownership_library_dirs_newgroup }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_group_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /usr/lib64/ recursively ansible.builtin.file: path: /usr/lib64/ follow: false state: directory recurse: true group: '{{ dir_group_ownership_library_dirs_newgroup }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_group_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that System Executable Have Root Ownership /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin All these directories should be owned by the root user. If any directory DIR in these directories is found to be owned by a user other than root, correct its ownership with the following command: $ sudo chown root DIR SRG-OS-000258-GPOS-00099 System binaries are executed by privileged users as well as system services, and restrictive permissions are necessary to ensure that their execution of these programs cannot be co-opted. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /bin/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /sbin/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /usr/bin/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /usr/sbin/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /usr/local/bin/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /usr/local/sbin/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi - name: Set the dir_ownership_binary_dirs_newown variable if represented by uid ansible.builtin.set_fact: dir_ownership_binary_dirs_newown: '0' tags: - configure_strategy - dir_ownership_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /bin/ recursively ansible.builtin.file: path: /bin/ follow: false state: directory recurse: true owner: '{{ dir_ownership_binary_dirs_newown }}' tags: - configure_strategy - dir_ownership_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /sbin/ recursively ansible.builtin.file: path: /sbin/ follow: false state: directory recurse: true owner: '{{ dir_ownership_binary_dirs_newown }}' tags: - configure_strategy - dir_ownership_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /usr/bin/ recursively ansible.builtin.file: path: /usr/bin/ follow: false state: directory recurse: true owner: '{{ dir_ownership_binary_dirs_newown }}' tags: - configure_strategy - dir_ownership_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /usr/sbin/ recursively ansible.builtin.file: path: /usr/sbin/ follow: false state: directory recurse: true owner: '{{ dir_ownership_binary_dirs_newown }}' tags: - configure_strategy - dir_ownership_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /usr/local/bin/ recursively ansible.builtin.file: path: /usr/local/bin/ follow: false state: directory recurse: true owner: '{{ dir_ownership_binary_dirs_newown }}' tags: - configure_strategy - dir_ownership_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /usr/local/sbin/ recursively ansible.builtin.file: path: /usr/local/sbin/ follow: false state: directory recurse: true owner: '{{ dir_ownership_binary_dirs_newown }}' tags: - configure_strategy - dir_ownership_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that Shared Library Directories Have Root Ownership System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default: /lib /lib64 /usr/lib /usr/lib64 Kernel modules, which can be added to the kernel during runtime, are also stored in /lib/modules. All files in these directories should be owned by the root user. If the directories, is found to be owned by a user other than root correct its ownership with the following command: $ sudo chown root DIR CM-5(6) CM-5(6).1 SRG-OS-000259-GPOS-00100 Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Proper ownership of library directories is necessary to protect the integrity of the system. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /lib/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /lib64/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /usr/lib/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; find -P /usr/lib64/ -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi - name: Set the dir_ownership_library_dirs_newown variable if represented by uid ansible.builtin.set_fact: dir_ownership_library_dirs_newown: '0' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /lib/ recursively ansible.builtin.file: path: /lib/ follow: false state: directory recurse: true owner: '{{ dir_ownership_library_dirs_newown }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /lib64/ recursively ansible.builtin.file: path: /lib64/ follow: false state: directory recurse: true owner: '{{ dir_ownership_library_dirs_newown }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /usr/lib/ recursively ansible.builtin.file: path: /usr/lib/ follow: false state: directory recurse: true owner: '{{ dir_ownership_library_dirs_newown }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /usr/lib64/ recursively ansible.builtin.file: path: /usr/lib64/ follow: false state: directory recurse: true owner: '{{ dir_ownership_library_dirs_newown }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that System Executable Directories Have Restrictive Permissions System executables are stored in the following directories by default: /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin These directories should not be group-writable or world-writable. If any directory DIR in these directories is found to be group-writable or world-writable, correct its permission with the following command: $ sudo chmod go-w DIR SRG-OS-000258-GPOS-00099 System binaries are executed by privileged users, as well as system services, and restrictive permissions are necessary to ensure execution of these programs cannot be co-opted. find -H /bin/ -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \; find -H /sbin/ -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \; find -H /usr/bin/ -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \; find -H /usr/sbin/ -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \; find -H /usr/local/bin/ -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \; find -H /usr/local/sbin/ -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \; - name: Find /bin/ file(s) recursively ansible.builtin.command: 'find -P /bin/ -perm /u+s,g+ws,o+wt -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /bin/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-ws,o-wt state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /sbin/ file(s) recursively ansible.builtin.command: 'find -P /sbin/ -perm /u+s,g+ws,o+wt -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /sbin/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-ws,o-wt state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/bin/ file(s) recursively ansible.builtin.command: 'find -P /usr/bin/ -perm /u+s,g+ws,o+wt -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/bin/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-ws,o-wt state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/sbin/ file(s) recursively ansible.builtin.command: 'find -P /usr/sbin/ -perm /u+s,g+ws,o+wt -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/sbin/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-ws,o-wt state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/local/bin/ file(s) recursively ansible.builtin.command: 'find -P /usr/local/bin/ -perm /u+s,g+ws,o+wt -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/local/bin/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-ws,o-wt state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/local/sbin/ file(s) recursively ansible.builtin.command: 'find -P /usr/local/sbin/ -perm /u+s,g+ws,o+wt -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/local/sbin/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-ws,o-wt state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - configure_strategy - dir_permissions_binary_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that Shared Library Directories Have Restrictive Permissions System-wide shared library directories, which contain are linked to executables during process load time or run time, are stored in the following directories by default: /lib /lib64 /usr/lib /usr/lib64 Kernel modules, which can be added to the kernel during runtime, are stored in /lib/modules. All sub-directories in these directories should not be group-writable or world-writable. If any file in these directories is found to be group-writable or world-writable, correct its permission with the following command: $ sudo chmod go-w DIR CIP-003-8 R6 CM-5 CM-5(6) CM-5(6).1 SRG-OS-000259-GPOS-00100 If the operating system were to allow any user to make changes to software libraries, then those changes might be implemented without undergoing the appropriate testing and approvals that are part of a robust change management process. This requirement applies to operating systems with software libraries that are accessible and configurable, as in the case of interpreted languages. Software libraries also include privileged programs which execute with escalated privileges. Only qualified and authorized individuals must be allowed to obtain access to information system components for purposes of initiating changes, including upgrades and modifications. find -H /lib/ -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \; find -H /lib64/ -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \; find -H /usr/lib/ -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \; find -H /usr/lib64/ -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \; - name: Find /lib/ file(s) recursively ansible.builtin.command: 'find -P /lib/ -perm /g+w,o+w -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /lib/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /lib64/ file(s) recursively ansible.builtin.command: 'find -P /lib64/ -perm /g+w,o+w -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /lib64/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/lib/ file(s) recursively ansible.builtin.command: 'find -P /usr/lib/ -perm /g+w,o+w -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/lib/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/lib64/ file(s) recursively ansible.builtin.command: 'find -P /usr/lib64/ -perm /g+w,o+w -type d ' register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/lib64/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: directory with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5 - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - dir_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that system commands files are group owned by root or a system account System commands files are stored in the following directories by default: /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin All files in these directories should be owned by the root group, or a system account. If the directory, or any file in these directories, is found to be owned by a group other than root or a a system account correct its ownership with the following command: $ sudo chgrp root FILE CM-5(6) CM-5(6).1 SRG-OS-000259-GPOS-00100 R50 If the operating system allows any user to make changes to software libraries, then those changes might be implemented without undergoing the appropriate testing and approvals that are part of a robust change management process. This requirement applies to operating systems with software libraries that are accessible and configurable, as in the case of interpreted languages. Software libraries also include privileged programs which execute with escalated privileges. Only qualified and authorized individuals must be allowed to obtain access to information system components for purposes of initiating changes, including upgrades and modifications. find -P /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin \! -group root -type f -exec chgrp root '{}' \; || true - name: Retrieve the system command files and set their group ownership to root ansible.builtin.command: find -P {{ item }} ! -group root -type f -exec chgrp root '{}' \; with_items: - /bin - /sbin - /usr/bin - /usr/sbin - /usr/local/bin - /usr/local/sbin changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - file_groupownership_system_commands_dirs - medium_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy Verify that System Executables Have Root Ownership System executables are stored in the following directories by default: /bin /sbin /usr/bin /usr/libexec /usr/local/bin /usr/local/sbin /usr/sbin All files in these directories should be owned by the root user. If any file FILE in these directories is found to be owned by a user other than root, correct its ownership with the following command: $ sudo chown root FILE 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-5(6) CM-5(6).1 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000259-GPOS-00100 R50 System binaries are executed by privileged users as well as system services, and restrictive permissions are necessary to ensure that their execution of these programs cannot be co-opted. - name: Read list of system executables without root ownership ansible.builtin.command: find /bin/ /usr/bin/ /usr/local/bin/ /sbin/ /usr/sbin/ /usr/local/sbin/ /usr/libexec \! -user root register: no_root_system_executables changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - file_ownership_binary_dirs - medium_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set ownership to root of system executables ansible.builtin.file: path: '{{ item }}' owner: root with_items: '{{ no_root_system_executables.stdout_lines }}' when: no_root_system_executables.stdout_lines | length > 0 tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - file_ownership_binary_dirs - medium_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy Verify that Shared Library Files Have Root Ownership System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default: /lib /lib64 /usr/lib /usr/lib64 Kernel modules, which can be added to the kernel during runtime, are also stored in /lib/modules. All files in these directories should be owned by the root user. If the directory, or any file in these directories, is found to be owned by a user other than root correct its ownership with the following command: $ sudo chown root FILE 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-5(6) CM-5(6).1 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000259-GPOS-00100 Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Proper ownership is necessary to protect the integrity of the system. newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /lib/ -type f ! -user 0 -regextype posix-extended -regex '^.*$' -exec chown --no-dereference "$newown" {} \; find -P /lib64/ -type f ! -user 0 -regextype posix-extended -regex '^.*$' -exec chown --no-dereference "$newown" {} \; find -P /usr/lib/ -type f ! -user 0 -regextype posix-extended -regex '^.*$' -exec chown --no-dereference "$newown" {} \; find -P /usr/lib64/ -type f ! -user 0 -regextype posix-extended -regex '^.*$' -exec chown --no-dereference "$newown" {} \; fi - name: Set the file_ownership_library_dirs_newown variable if represented by uid ansible.builtin.set_fact: file_ownership_library_dirs_newown: '0' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /lib/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /lib/ -type f ! -user 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /lib/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_library_dirs_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /lib64/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /lib64/ -type f ! -user 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /lib64/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_library_dirs_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/lib/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /usr/lib/ -type f ! -user 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /usr/lib/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_library_dirs_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/lib64/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /usr/lib64/ -type f ! -user 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /usr/lib64/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_library_dirs_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_ownership_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that System Executables Have Restrictive Permissions System executables are stored in the following directories by default: /bin /sbin /usr/bin /usr/libexec /usr/local/bin /usr/local/sbin /usr/sbin All files in these directories should not be group-writable or world-writable. If any file FILE in these directories is found to be group-writable or world-writable, correct its permission with the following command: $ sudo chmod go-w FILE 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-5(6) CM-5(6).1 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000259-GPOS-00100 R50 System binaries are executed by privileged users, as well as system services, and restrictive permissions are necessary to ensure execution of these programs cannot be co-opted. - name: Read list of world and group writable system executables ansible.builtin.command: find -L /bin /usr/bin /usr/local/bin /sbin /usr/sbin /usr/local/sbin /usr/libexec -perm /022 \( -type l -o -type f \) register: world_writable_library_files changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - file_permissions_binary_dirs - medium_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Remove world/group writability of system executables ansible.builtin.file: path: '{{ item }}' mode: go-w state: file with_items: '{{ world_writable_library_files.stdout_lines }}' when: world_writable_library_files.stdout_lines | length > 0 tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - file_permissions_binary_dirs - medium_complexity - medium_disruption - medium_severity - no_reboot_needed - restrict_strategy Verify that Shared Library Files Have Restrictive Permissions System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default: /lib /lib64 /usr/lib /usr/lib64 Kernel modules, which can be added to the kernel during runtime, are stored in /lib/modules. All files in these directories should not be group-writable or world-writable. If any file in these directories is found to be group-writable or world-writable, correct its permission with the following command: $ sudo chmod go-w FILE 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) CM-5(6) CM-5(6).1 AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000259-GPOS-00100 Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Restrictive permissions are necessary to protect the integrity of the system. find -P /lib/ -perm /g+w,o+w -type f -regextype posix-extended -regex '^.*$' -exec chmod g-w,o-w {} \; find -P /lib64/ -perm /g+w,o+w -type f -regextype posix-extended -regex '^.*$' -exec chmod g-w,o-w {} \; find -P /usr/lib/ -perm /g+w,o+w -type f -regextype posix-extended -regex '^.*$' -exec chmod g-w,o-w {} \; find -P /usr/lib64/ -perm /g+w,o+w -type f -regextype posix-extended -regex '^.*$' -exec chmod g-w,o-w {} \; - name: Find /lib/ file(s) recursively ansible.builtin.command: find -P /lib/ -perm /g+w,o+w -type f -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /lib/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /lib64/ file(s) recursively ansible.builtin.command: find -P /lib64/ -perm /g+w,o+w -type f -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /lib64/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/lib/ file(s) recursively ansible.builtin.command: find -P /usr/lib/ -perm /g+w,o+w -type f -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/lib/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /usr/lib64/ file(s) recursively ansible.builtin.command: find -P /usr/lib64/ -perm /g+w,o+w -type f -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /usr/lib64/ file(s) ansible.builtin.file: path: '{{ item }}' mode: g-w,o-w state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - NIST-800-53-CM-6(a) - configure_strategy - file_permissions_library_dirs - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify the system-wide library files in directories "/lib", "/lib64", "/usr/lib/" and "/usr/lib64" are group-owned by root. System-wide library files are stored in the following directories by default: /lib /lib64 /usr/lib /usr/lib64 All system-wide shared library files should be protected from unauthorised access. If any of these files is not group-owned by root, correct its group-owner with the following command: $ sudo chgrp root FILE CM-5(6) CM-5(6).1 SRG-OS-000259-GPOS-00100 If the operating system were to allow any user to make changes to software libraries, then those changes might be implemented without undergoing the appropriate testing and approvals that are part of a robust change management process. This requirement applies to operating systems with software libraries that are accessible and configurable, as in the case of interpreted languages. Software libraries also include privileged programs which execute with escalated privileges. Only qualified and authorized individuals must be allowed to obtain access to information system components for purposes of initiating changes, including upgrades and modifications. newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /lib/ -type f ! -group 0 -regextype posix-extended -regex '^.*$' -exec chgrp --no-dereference "$newgroup" {} \; find -P /lib64/ -type f ! -group 0 -regextype posix-extended -regex '^.*$' -exec chgrp --no-dereference "$newgroup" {} \; find -P /usr/lib/ -type f ! -group 0 -regextype posix-extended -regex '^.*$' -exec chgrp --no-dereference "$newgroup" {} \; find -P /usr/lib64/ -type f ! -group 0 -regextype posix-extended -regex '^.*$' -exec chgrp --no-dereference "$newgroup" {} \; fi - name: Set the root_permissions_syslibrary_files_newgroup variable if represented by gid ansible.builtin.set_fact: root_permissions_syslibrary_files_newgroup: '0' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Find /lib/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /lib/ -type f ! -group 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Ensure group owner on /lib/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ root_permissions_syslibrary_files_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Find /lib64/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /lib64/ -type f ! -group 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Ensure group owner on /lib64/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ root_permissions_syslibrary_files_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Find /usr/lib/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /usr/lib/ -type f ! -group 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Ensure group owner on /usr/lib/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ root_permissions_syslibrary_files_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Find /usr/lib64/ file(s) matching ^.*$ recursively ansible.builtin.command: find -P /usr/lib64/ -type f ! -group 0 -regextype posix-extended -regex "^.*$" register: files_found changed_when: false failed_when: false check_mode: false tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files - name: Ensure group owner on /usr/lib64/ file(s) matching ^.*$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ root_permissions_syslibrary_files_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' tags: - NIST-800-53-CM-5(6) - NIST-800-53-CM-5(6).1 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - root_permissions_syslibrary_files Restrict Dynamic Mounting and Unmounting of Filesystems Linux includes a number of facilities for the automated addition and removal of filesystems on a running system. These facilities may be necessary in many environments, but this capability also carries some risk -- whether direct risk from allowing users to introduce arbitrary filesystems, or risk that software flaws in the automated mount facility itself could allow an attacker to compromise the system. This command can be used to list the types of filesystems that are available to the currently executing kernel: $ find /lib/modules/`uname -r`/kernel/fs -type f -name '*.ko' If these filesystems are not required then they can be explicitly disabled in a configuratio file in /etc/modprobe.d. Disable the Automounter The autofs daemon mounts and unmounts filesystems, such as user home directories shared via NFS, on demand. In addition, autofs can be used to handle removable media, and the default configuration provides the cdrom device as /misc/cd. However, this method of providing access to removable media is not common, so autofs can almost always be disabled if NFS is not in use. Even if NFS is required, it may be possible to configure filesystem mounts statically by editing /etc/fstab rather than relying on the automounter. The autofs service can be disabled with the following command: $ sudo systemctl mask --now autofs.service 1 12 15 16 5 APO13.01 DSS01.04 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.4.6 164.308(a)(3)(i) 164.308(a)(3)(ii)(A) 164.310(d)(1) 164.310(d)(2) 164.312(a)(1) 164.312(a)(2)(iv) 164.312(b) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.6 A.11.2.6 A.13.1.1 A.13.2.1 A.18.1.4 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CM-7(a) CM-7(b) CM-6(a) MP-7 PR.AC-1 PR.AC-3 PR.AC-6 PR.AC-7 SRG-OS-000114-GPOS-00059 SRG-OS-000378-GPOS-00163 SRG-OS-000480-GPOS-00227 2.1.1 Disabling the automounter permits the administrator to statically control filesystem mounting through /etc/fstab. Additionally, automatically mounting filesystems permits easy introduction of unknown devices, thereby facilitating malicious activity. # Remediation is applicable only in certain platforms if ( rpm --quiet -q autofs && rpm --quiet -q kernel ); then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'autofs.service' fi "$SYSTEMCTL_EXEC" disable 'autofs.service' "$SYSTEMCTL_EXEC" mask 'autofs.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files autofs.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'autofs.socket' fi "$SYSTEMCTL_EXEC" mask 'autofs.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'autofs.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_autofs_disabled - name: Disable the Automounter - Disable service autofs block: - name: Disable the Automounter - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable the Automounter - Ensure autofs.service is Masked ansible.builtin.systemd: name: autofs.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("autofs.service", multiline=True) - name: Unit Socket Exists - autofs.socket ansible.builtin.command: systemctl -q list-unit-files autofs.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable the Automounter - Disable Socket autofs ansible.builtin.systemd: name: autofs.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("autofs.socket", multiline=True) tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_autofs_disabled - special_service_block when: ( "autofs" in ansible_facts.packages and "kernel" in ansible_facts.packages ) include disable_autofs class disable_autofs { service {'autofs': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: autofs.service enabled: false mask: true - name: autofs.socket enabled: false mask: true [customizations.services] masked = ["autofs"] service disable autofs Assign Password to Prevent Changes to Boot Firmware Configuration Assign a password to the system boot firmware (historically called BIOS on PC systems) to require a password for any configuration changes. Assigning a password to the system boot firmware prevents anyone with physical access from configuring the system to boot from local media and circumvent the operating system's access controls. For systems in physically secure locations, such as a data center or Sensitive Compartmented Information Facility (SCIF), this risk must be weighed against the risk of administrative personnel being unable to conduct recovery operations in a timely fashion. Disable Booting from USB Devices in Boot Firmware Configure the system boot firmware (historically called BIOS on PC systems) to disallow booting from USB drives. 12 16 APO13.01 DSS01.04 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 4.3.3.2.2 4.3.3.5.2 4.3.3.6.6 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.13 SR 1.2 SR 1.4 SR 1.5 SR 1.9 SR 2.1 SR 2.6 A.11.2.6 A.13.1.1 A.13.2.1 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 MP-7 CM-7(b) CM-6(a) PR.AC-3 PR.AC-6 Booting a system from a USB device would allow an attacker to circumvent any security measures provided by the operating system. Attackers could mount partitions and modify the configuration of the OS. Disable Kernel Support for USB via Bootloader Configuration All USB support can be disabled by adding the nousb argument to the kernel's boot loader configuration. To do so, add the argument nousb to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain nousb as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) nousb" Disabling all kernel support for USB will cause problems for systems with USB-based keyboards, mice, or printers. This configuration is infeasible for systems which require USB devices, which is common. 12 16 APO13.01 DSS01.04 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 164.308(a)(3)(i) 164.308(a)(3)(ii)(A) 164.310(d)(1) 164.310(d)(2) 164.312(a)(1) 164.312(a)(2)(iv) 164.312(b) 4.3.3.2.2 4.3.3.5.2 4.3.3.6.6 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.13 SR 1.2 SR 1.4 SR 1.5 SR 1.9 SR 2.1 SR 2.6 A.11.2.6 A.13.1.1 A.13.2.1 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 MP-7 CM-6(a) PR.AC-3 PR.AC-6 Disabling the USB subsystem within the Linux kernel at system boot will protect against potentially malicious USB devices, although it is only practical in specialized systems. # Remediation is applicable only in certain platforms if rpm --quiet -q grub2-common; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "nousb" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"nousb=[^\"]*\"(.*]\s*)/\1\"nousb\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"nousb\"]" >> "$KARGS_DIR/10-nousb.toml" fi else grubby --update-kernel=ALL --args=nousb fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-MP-7 - grub2_nousb_argument - low_disruption - medium_complexity - reboot_required - restrict_strategy - unknown_severity - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="nousb" when: '"grub2-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-MP-7 - grub2_nousb_argument - low_disruption - medium_complexity - reboot_required - restrict_strategy - unknown_severity [customizations.kernel] append = "nousb" bootloader nousb Disable Mounting of cramfs To configure the system to prevent the cramfs kernel module from being loaded, add the following line to the file /etc/modprobe.d/cramfs.conf: install cramfs /bin/false This effectively prevents usage of this uncommon filesystem. The cramfs filesystem type is a compressed read-only Linux filesystem embedded in small footprint systems. A cramfs image can be used without having to first decompress the image. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 SRG-OS-000095-GPOS-00049 1.1.1.1 Removing support for unneeded filesystem types reduces the local attack surface of the server. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install cramfs" /etc/modprobe.d/cramfs.conf ; then sed -i 's#^install cramfs.*#install cramfs /bin/false#g' /etc/modprobe.d/cramfs.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/cramfs.conf echo "install cramfs /bin/false" >> /etc/modprobe.d/cramfs.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_cramfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'cramfs' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/cramfs.conf regexp: install\s+cramfs line: install cramfs /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_cramfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20cramfs%20/bin/false%0Ablacklist%20cramfs%0A mode: 0644 path: /etc/modprobe.d/cramfs.conf overwrite: true Disable Mounting of freevxfs To configure the system to prevent the freevxfs kernel module from being loaded, add the following line to the file /etc/modprobe.d/freevxfs.conf: install freevxfs /bin/false This effectively prevents usage of this uncommon filesystem. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 1.1.1.2 Linux kernel modules which implement filesystems that are not needed by the local system should be disabled. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install freevxfs" /etc/modprobe.d/freevxfs.conf ; then sed -i 's#^install freevxfs.*#install freevxfs /bin/false#g' /etc/modprobe.d/freevxfs.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/freevxfs.conf echo "install freevxfs /bin/false" >> /etc/modprobe.d/freevxfs.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_freevxfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'freevxfs' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/freevxfs.conf regexp: install\s+freevxfs line: install freevxfs /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_freevxfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20freevxfs%20/bin/false%0Ablacklist%20freevxfs%0A mode: 0644 path: /etc/modprobe.d/freevxfs.conf overwrite: true Disable Mounting of hfs To configure the system to prevent the hfs kernel module from being loaded, add the following line to the file /etc/modprobe.d/hfs.conf: install hfs /bin/false This effectively prevents usage of this uncommon filesystem. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 1.1.1.3 Linux kernel modules which implement filesystems that are not needed by the local system should be disabled. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install hfs" /etc/modprobe.d/hfs.conf ; then sed -i 's#^install hfs.*#install hfs /bin/false#g' /etc/modprobe.d/hfs.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/hfs.conf echo "install hfs /bin/false" >> /etc/modprobe.d/hfs.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_hfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'hfs' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/hfs.conf regexp: install\s+hfs line: install hfs /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_hfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20hfs%20/bin/false%0Ablacklist%20hfs%0A mode: 0644 path: /etc/modprobe.d/hfs.conf overwrite: true Disable Mounting of hfsplus To configure the system to prevent the hfsplus kernel module from being loaded, add the following line to the file /etc/modprobe.d/hfsplus.conf: install hfsplus /bin/false This effectively prevents usage of this uncommon filesystem. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 1.1.1.4 Linux kernel modules which implement filesystems that are not needed by the local system should be disabled. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install hfsplus" /etc/modprobe.d/hfsplus.conf ; then sed -i 's#^install hfsplus.*#install hfsplus /bin/false#g' /etc/modprobe.d/hfsplus.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/hfsplus.conf echo "install hfsplus /bin/false" >> /etc/modprobe.d/hfsplus.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_hfsplus_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'hfsplus' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/hfsplus.conf regexp: install\s+hfsplus line: install hfsplus /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_hfsplus_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20hfsplus%20/bin/false%0Ablacklist%20hfsplus%0A mode: 0644 path: /etc/modprobe.d/hfsplus.conf overwrite: true Disable Mounting of jffs2 To configure the system to prevent the jffs2 kernel module from being loaded, add the following line to the file /etc/modprobe.d/jffs2.conf: install jffs2 /bin/false This effectively prevents usage of this uncommon filesystem. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 1.1.1.5 Linux kernel modules which implement filesystems that are not needed by the local system should be disabled. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install jffs2" /etc/modprobe.d/jffs2.conf ; then sed -i 's#^install jffs2.*#install jffs2 /bin/false#g' /etc/modprobe.d/jffs2.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/jffs2.conf echo "install jffs2 /bin/false" >> /etc/modprobe.d/jffs2.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_jffs2_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'jffs2' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/jffs2.conf regexp: install\s+jffs2 line: install jffs2 /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_jffs2_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20jffs2%20/bin/false%0Ablacklist%20jffs2%0A mode: 0644 path: /etc/modprobe.d/jffs2.conf overwrite: true Ensure overlayfs kernel module is not available To configure the system to prevent the overlayfs kernel module from being loaded, add the following line to the file /etc/modprobe.d/overlayfs.conf: install overlayfs /bin/false overlayfs is a Linux filesystem that layers multiple filesystems to create a single unified view which allows a user to "merge" several mount points into a unified filesystem. 1.1.1.6 The overlayfs has known CVE's. Disabling the overlayfs reduces the local attack surface by removing support for unnecessary filesystem types and mitigates potential risks associated with unauthorized execution of setuid files, enhancing the overall system security. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install overlayfs" /etc/modprobe.d/overlayfs.conf ; then sed -i 's#^install overlayfs.*#install overlayfs /bin/false#g' /etc/modprobe.d/overlayfs.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/overlayfs.conf echo "install overlayfs /bin/false" >> /etc/modprobe.d/overlayfs.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - kernel_module_overlayfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'overlayfs' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/overlayfs.conf regexp: install\s+overlayfs line: install overlayfs /bin/false when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - kernel_module_overlayfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20overlayfs%20/bin/false%0Ablacklist%20overlayfs%0A mode: 0644 path: /etc/modprobe.d/overlayfs.conf overwrite: true Disable Mounting of squashfs To configure the system to prevent the squashfs kernel module from being loaded, add the following line to the file /etc/modprobe.d/squashfs.conf: install squashfs /bin/false This effectively prevents usage of this uncommon filesystem. The squashfs filesystem type is a compressed read-only Linux filesystem embedded in small footprint systems (similar to cramfs). A squashfs image can be used without having to first decompress the image. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 1.1.1.7 Removing support for unneeded filesystem types reduces the local attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install squashfs" /etc/modprobe.d/squashfs.conf ; then sed -i 's#^install squashfs.*#install squashfs /bin/false#g' /etc/modprobe.d/squashfs.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/squashfs.conf echo "install squashfs /bin/false" >> /etc/modprobe.d/squashfs.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_squashfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'squashfs' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/squashfs.conf regexp: install\s+squashfs line: install squashfs /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_squashfs_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20squashfs%20/bin/false%0Ablacklist%20squashfs%0A mode: 0644 path: /etc/modprobe.d/squashfs.conf overwrite: true Disable Mounting of udf To configure the system to prevent the udf kernel module from being loaded, add the following line to the file /etc/modprobe.d/udf.conf: install udf /bin/false This effectively prevents usage of this uncommon filesystem. The udf filesystem type is the universal disk format used to implement the ISO/IEC 13346 and ECMA-167 specifications. This is an open vendor filesystem type for data storage on a broad range of media. This filesystem type is neccessary to support writing DVDs and newer optical disc formats. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 1.1.1.8 Removing support for unneeded filesystem types reduces the local attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install udf" /etc/modprobe.d/udf.conf ; then sed -i 's#^install udf.*#install udf /bin/false#g' /etc/modprobe.d/udf.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/udf.conf echo "install udf /bin/false" >> /etc/modprobe.d/udf.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_udf_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'udf' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/udf.conf regexp: install\s+udf line: install udf /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_udf_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20udf%20/bin/false%0Ablacklist%20udf%0A mode: 0644 path: /etc/modprobe.d/udf.conf overwrite: true Disable Modprobe Loading of USB Storage Driver To prevent USB storage devices from being used, configure the kernel module loading system to prevent automatic loading of the USB storage driver. To configure the system to prevent the usb-storage kernel module from being loaded, add the following line to the file /etc/modprobe.d/usb-storage.conf: install usb-storage /bin/false This will prevent the modprobe program from loading the usb-storage module, but will not prevent an administrator (or another program) from using the insmod program to load the module manually. 1 12 15 16 5 APO13.01 DSS01.04 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.1.21 164.308(a)(3)(i) 164.308(a)(3)(ii)(A) 164.310(d)(1) 164.310(d)(2) 164.312(a)(1) 164.312(a)(2)(iv) 164.312(b) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.6 A.11.2.6 A.13.1.1 A.13.2.1 A.18.1.4 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CM-7(a) CM-7(b) CM-6(a) MP-7 PR.AC-1 PR.AC-3 PR.AC-6 PR.AC-7 SRG-OS-000114-GPOS-00059 SRG-OS-000378-GPOS-00163 SRG-OS-000480-GPOS-00227 SRG-APP-000141-CTR-000315 1.1.1.10 3.4.2 3.4 USB storage devices such as thumb drives can be used to introduce malicious software. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install usb-storage" /etc/modprobe.d/usb-storage.conf ; then sed -i 's#^install usb-storage.*#install usb-storage /bin/false#g' /etc/modprobe.d/usb-storage.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/usb-storage.conf echo "install usb-storage /bin/false" >> /etc/modprobe.d/usb-storage.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.21 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - disable_strategy - kernel_module_usb-storage_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'usb-storage' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/usb-storage.conf regexp: install\s+usb-storage line: install usb-storage /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.21 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - PCI-DSSv4-3.4 - PCI-DSSv4-3.4.2 - disable_strategy - kernel_module_usb-storage_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20usb-storage%20/bin/false%0Ablacklist%20usb-storage%0A mode: 0644 path: /etc/modprobe.d/usb-storage.conf overwrite: true Disable Mounting of vFAT filesystems To configure the system to prevent the vfat kernel module from being loaded, add the following line to the file /etc/modprobe.d/vfat.conf: install vfat /bin/false This effectively prevents usage of this uncommon filesystem. The vFAT filesystem format is primarily used on older windows systems and portable USB drives or flash modules. It comes in three types FAT12, FAT16, and FAT32 all of which are supported by the vfat kernel module. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 3.4.6 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Removing support for unneeded filesystems reduces the local attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install vfat" /etc/modprobe.d/vfat.conf ; then sed -i 's#^install vfat.*#install vfat /bin/false#g' /etc/modprobe.d/vfat.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/vfat.conf echo "install vfat /bin/false" >> /etc/modprobe.d/vfat.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_vfat_disabled - low_complexity - low_severity - medium_disruption - reboot_required - name: Ensure kernel module 'vfat' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/vfat.conf regexp: install\s+vfat line: install vfat /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.4.6 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - kernel_module_vfat_disabled - low_complexity - low_severity - medium_disruption - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20vfat%20/bin/false%0Ablacklist%20vfat%0A mode: 0644 path: /etc/modprobe.d/vfat.conf overwrite: true Restrict Partition Mount Options System partitions can be mounted with certain options that limit what files on those partitions can do. These options are set in the /etc/fstab configuration file, and can be used to make certain types of malicious behavior more difficult. Value for hidepid option The hidepid mount option is applicable to /proc and is used to control who can access the information in /proc/[pid] directories. The option can have one of the following values: 0: Everybody may access all /proc/[pid] directories. 1: Users may not access files and subdirectories inside any /proc/[pid] directories but their own. The /proc/[pid] directories themselves remain visible. 2: Same as for mode 1, but in addition the /proc/[pid] directories belonging to other users become invisible. 0 noaccess invisible 1 2 2 Removable Partition This value is used by the checks mount_option_nodev_removable_partitions, mount_option_nodev_removable_partitions, and mount_option_nodev_removable_partitions to ensure that the correct mount options are set on partitions mounted from removable media such as CD-ROMs, USB keys, and floppy drives. This value should be modified to reflect any removable partitions that are required on the local system. /dev/cdrom /dev/cdrom Add noauto Option to /boot The noauto mount option is used to prevent automatic mounting of th /boot partition. Add the noauto option to the fourth column of /etc/fstab for the line which controls mounting of /boot. Although contents of the /boot partition should not be needed during normal system operation, they might need to be accessible during system maintenance and upgrades. Make sure that applying this rule will not break upgrade or maintenance processes affecting the system. The /boot partition contains the kernel and the bootloader. Access to the partition after the boot process finishes should not be needed. Files contained within this partition can be analysed and gained information can be used for exploit creation. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { # the mount point /boot has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noauto)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /boot defaults,${previous_mount_opts}noauto 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noauto"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noauto|" /etc/fstab fi if mkdir -p "/boot"; then if mountpoint -q "/boot"; then mount -o remount --target "/boot" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noauto - no_reboot_needed - name: 'Add noauto Option to /boot: Check information associated to mountpoint' command: findmnt --fstab '/boot' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noauto - no_reboot_needed - name: 'Add noauto Option to /boot: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noauto - no_reboot_needed - name: 'Add noauto Option to /boot: If /boot not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /boot - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noauto - no_reboot_needed - name: 'Add noauto Option to /boot: Make sure noauto option is part of the to /boot options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noauto'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "noauto" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noauto - no_reboot_needed - name: 'Add noauto Option to /boot: Ensure /boot is mounted with noauto option' mount: path: /boot src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noauto - no_reboot_needed part /boot --mountoptions="noauto" Add nodev Option to /boot The nodev mount option can be used to prevent device files from being created in /boot. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /boot. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { # the mount point /boot has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /boot defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/boot"; then if mountpoint -q "/boot"; then mount -o remount --target "/boot" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nodev - no_reboot_needed - name: 'Add nodev Option to /boot: Check information associated to mountpoint' command: findmnt --fstab '/boot' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nodev - no_reboot_needed - name: 'Add nodev Option to /boot: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nodev - no_reboot_needed - name: 'Add nodev Option to /boot: If /boot not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /boot - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nodev - no_reboot_needed - name: 'Add nodev Option to /boot: Make sure nodev option is part of the to /boot options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "nodev" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nodev - no_reboot_needed - name: 'Add nodev Option to /boot: Ensure /boot is mounted with nodev option' mount: path: /boot src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nodev - no_reboot_needed part /boot --mountoptions="nodev" Add noexec Option to /boot The noexec mount option can be used to prevent binaries from being executed out of /boot. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /boot. R28 The /boot partition contains the kernel and the bootloader. No binaries should be executed from this partition after the booting process finishes. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { # the mount point /boot has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /boot defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/boot"; then if mountpoint -q "/boot"; then mount -o remount --target "/boot" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noexec - no_reboot_needed - name: 'Add noexec Option to /boot: Check information associated to mountpoint' command: findmnt --fstab '/boot' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noexec - no_reboot_needed - name: 'Add noexec Option to /boot: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noexec - no_reboot_needed - name: 'Add noexec Option to /boot: If /boot not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /boot - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noexec - no_reboot_needed - name: 'Add noexec Option to /boot: Make sure noexec option is part of the to /boot options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "noexec" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noexec - no_reboot_needed - name: 'Add noexec Option to /boot: Ensure /boot is mounted with noexec option' mount: path: /boot src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_noexec - no_reboot_needed part /boot --mountoptions="noexec" Add nosuid Option to /boot The nosuid mount option can be used to prevent execution of setuid programs in /boot. The SUID and SGID permissions should not be required on the boot partition. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /boot. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 SRG-OS-000480-GPOS-00227 R28 The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from boot partitions. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { # the mount point /boot has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /boot defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/boot"; then if mountpoint -q "/boot"; then mount -o remount --target "/boot" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nosuid - no_reboot_needed - name: 'Add nosuid Option to /boot: Check information associated to mountpoint' command: findmnt --fstab '/boot' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nosuid - no_reboot_needed - name: 'Add nosuid Option to /boot: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nosuid - no_reboot_needed - name: 'Add nosuid Option to /boot: If /boot not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /boot - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nosuid - no_reboot_needed - name: 'Add nosuid Option to /boot: Make sure nosuid option is part of the to /boot options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "nosuid" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nosuid - no_reboot_needed - name: 'Add nosuid Option to /boot: Ensure /boot is mounted with nosuid option' mount: path: /boot src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_boot_nosuid - no_reboot_needed part /boot --mountoptions="nosuid" Add nodev Option to /dev/shm The nodev mount option can be used to prevent creation of device files in /dev/shm. Legitimate character and block devices should not exist within temporary directories like /dev/shm. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /dev/shm. 11 13 14 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS05.06 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 1.1.2.2.2 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /dev/shm)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="tmpfs" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/dev/shm"; then if mountpoint -q "/dev/shm"; then mount -o remount --target "/dev/shm" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nodev - no_reboot_needed - name: 'Add nodev Option to /dev/shm: Check information associated to mountpoint' command: findmnt '/dev/shm' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nodev - no_reboot_needed - name: 'Add nodev Option to /dev/shm: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nodev - no_reboot_needed - name: 'Add nodev Option to /dev/shm: If /dev/shm not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /dev/shm - tmpfs - tmpfs - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nodev - no_reboot_needed - name: 'Add nodev Option to /dev/shm: Make sure nodev option is part of the to /dev/shm options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "nodev" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nodev - no_reboot_needed - name: 'Add nodev Option to /dev/shm: Ensure /dev/shm is mounted with nodev option' mount: path: /dev/shm src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nodev - no_reboot_needed Add noexec Option to /dev/shm The noexec mount option can be used to prevent binaries from being executed out of /dev/shm. It can be dangerous to allow the execution of binaries from world-writable temporary storage directories such as /dev/shm. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /dev/shm. 11 13 14 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS05.06 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 1.1.2.2.4 Allowing users to execute binaries from world-writable directories such as /dev/shm can expose the system to potential compromise. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /dev/shm)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="tmpfs" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/dev/shm"; then if mountpoint -q "/dev/shm"; then mount -o remount --target "/dev/shm" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_noexec - no_reboot_needed - name: 'Add noexec Option to /dev/shm: Check information associated to mountpoint' command: findmnt '/dev/shm' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_noexec - no_reboot_needed - name: 'Add noexec Option to /dev/shm: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_noexec - no_reboot_needed - name: 'Add noexec Option to /dev/shm: If /dev/shm not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /dev/shm - tmpfs - tmpfs - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_noexec - no_reboot_needed - name: 'Add noexec Option to /dev/shm: Make sure noexec option is part of the to /dev/shm options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "noexec" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_noexec - no_reboot_needed - name: 'Add noexec Option to /dev/shm: Ensure /dev/shm is mounted with noexec option' mount: path: /dev/shm src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_noexec - no_reboot_needed Add nosuid Option to /dev/shm The nosuid mount option can be used to prevent execution of setuid programs in /dev/shm. The SUID and SGID permissions should not be required in these world-writable directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /dev/shm. 11 13 14 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS05.06 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 1.1.2.2.3 The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from temporary storage partitions. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /dev/shm)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="tmpfs" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/dev/shm"; then if mountpoint -q "/dev/shm"; then mount -o remount --target "/dev/shm" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nosuid - no_reboot_needed - name: 'Add nosuid Option to /dev/shm: Check information associated to mountpoint' command: findmnt '/dev/shm' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nosuid - no_reboot_needed - name: 'Add nosuid Option to /dev/shm: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nosuid - no_reboot_needed - name: 'Add nosuid Option to /dev/shm: If /dev/shm not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /dev/shm - tmpfs - tmpfs - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nosuid - no_reboot_needed - name: 'Add nosuid Option to /dev/shm: Make sure nosuid option is part of the to /dev/shm options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "nosuid" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nosuid - no_reboot_needed - name: 'Add nosuid Option to /dev/shm: Ensure /dev/shm is mounted with nosuid option' mount: path: /dev/shm src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_dev_shm_nosuid - no_reboot_needed Add grpquota Option to /home The grpquota mount option allows for the filesystem to have disk quotas configured. Add the grpquota option to the fourth column of /etc/fstab for the line which controls mounting of /home. The quota options for XFS file systems can only be activated when mounting the partition. It is not possible to enable them by remounting an already mounted partition. Therefore, if the desired options were not defined before mounting the partition, dismount and mount it again to apply the quota options. CM-6(b) To ensure the availability of disk space on /home, it is important to limit the impact a single user or group can cause for other users (or the wider system) by intentionally or accidentally filling up the partition. Quotas can also be applied to inodes for filesystems where inode exhaustion is a concern. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/home" > /dev/null || findmnt --fstab "/home" > /dev/null; }; then function perform_remediation { # the mount point /home has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/home")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/home' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /home in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /home)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|grpquota)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /home defaults,${previous_mount_opts}grpquota 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "grpquota"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,grpquota|" /etc/fstab fi if mkdir -p "/home"; then if mountpoint -q "/home"; then mount -o remount --target "/home" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_grpquota - no_reboot_needed - name: 'Add grpquota Option to /home: Check information associated to mountpoint' command: findmnt --fstab '/home' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_grpquota - no_reboot_needed - name: 'Add grpquota Option to /home: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_grpquota - no_reboot_needed - name: 'Add grpquota Option to /home: If /home not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /home - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_grpquota - no_reboot_needed - name: 'Add grpquota Option to /home: Make sure grpquota option is part of the to /home options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',grpquota'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "grpquota" not in mount_info.options tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_grpquota - no_reboot_needed - name: 'Add grpquota Option to /home: Ensure /home is mounted with grpquota option' mount: path: /home src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_grpquota - no_reboot_needed part /home --mountoptions="grpquota" Add nodev Option to /home The nodev mount option can be used to prevent device files from being created in /home. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /home. SRG-OS-000368-GPOS-00154 1.1.2.3.2 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/home" > /dev/null || findmnt --fstab "/home" > /dev/null; }; then function perform_remediation { # the mount point /home has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/home")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/home' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /home in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /home)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /home defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/home"; then if mountpoint -q "/home"; then mount -o remount --target "/home" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - mount_option_home_nodev - no_reboot_needed - unknown_severity - name: 'Add nodev Option to /home: Check information associated to mountpoint' command: findmnt --fstab '/home' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - mount_option_home_nodev - no_reboot_needed - unknown_severity - name: 'Add nodev Option to /home: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - mount_option_home_nodev - no_reboot_needed - unknown_severity - name: 'Add nodev Option to /home: If /home not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /home - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - mount_option_home_nodev - no_reboot_needed - unknown_severity - name: 'Add nodev Option to /home: Make sure nodev option is part of the to /home options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nodev" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - mount_option_home_nodev - no_reboot_needed - unknown_severity - name: 'Add nodev Option to /home: Ensure /home is mounted with nodev option' mount: path: /home src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - mount_option_home_nodev - no_reboot_needed - unknown_severity part /home --mountoptions="nodev" Add noexec Option to /home The noexec mount option can be used to prevent binaries from being executed out of /home. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /home. CM-6(b) SRG-OS-000480-GPOS-00227 R28 The /home directory contains data of individual users. Binaries in this directory should not be considered as trusted and users should not be able to execute them. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { # the mount point /home has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/home")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/home' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /home in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /home)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /home defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/home"; then if mountpoint -q "/home"; then mount -o remount --target "/home" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_noexec - no_reboot_needed - name: 'Add noexec Option to /home: Check information associated to mountpoint' command: findmnt --fstab '/home' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_noexec - no_reboot_needed - name: 'Add noexec Option to /home: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_noexec - no_reboot_needed - name: 'Add noexec Option to /home: If /home not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /home - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_noexec - no_reboot_needed - name: 'Add noexec Option to /home: Make sure noexec option is part of the to /home options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "noexec" not in mount_info.options tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_noexec - no_reboot_needed - name: 'Add noexec Option to /home: Ensure /home is mounted with noexec option' mount: path: /home src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_noexec - no_reboot_needed part /home --mountoptions="noexec" Add nosuid Option to /home The nosuid mount option can be used to prevent execution of setuid programs in /home. The SUID and SGID permissions should not be required in these user data directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /home. 11 13 14 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS05.06 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 SRG-OS-000480-GPOS-00227 R28 1.1.2.3.3 The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from user home directory partitions. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/home" > /dev/null || findmnt --fstab "/home" > /dev/null; }; then function perform_remediation { # the mount point /home has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/home")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/home' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /home in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /home)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /home defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/home"; then if mountpoint -q "/home"; then mount -o remount --target "/home" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_nosuid - no_reboot_needed - name: 'Add nosuid Option to /home: Check information associated to mountpoint' command: findmnt --fstab '/home' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_nosuid - no_reboot_needed - name: 'Add nosuid Option to /home: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_nosuid - no_reboot_needed - name: 'Add nosuid Option to /home: If /home not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /home - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_nosuid - no_reboot_needed - name: 'Add nosuid Option to /home: Make sure nosuid option is part of the to /home options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_nosuid - no_reboot_needed - name: 'Add nosuid Option to /home: Ensure /home is mounted with nosuid option' mount: path: /home src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_nosuid - no_reboot_needed part /home --mountoptions="nosuid" Add usrquota Option to /home The usrquota mount option allows for the filesystem to have disk quotas configured. Add the usrquota option to the fourth column of /etc/fstab for the line which controls mounting of /home. The quota options for XFS file systems can only be activated when mounting the partition. It is not possible to enable them by remounting an already mounted partition. Therefore, if the desired options were not defined before mounting the partition, dismount and mount it again to apply the quota options. CM-6(b) To ensure the availability of disk space on /home, it is important to limit the impact a single user or group can cause for other users (or the wider system) by intentionally or accidentally filling up the partition. Quotas can also be applied to inodes for filesystems where inode exhaustion is a concern. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/home" > /dev/null || findmnt --fstab "/home" > /dev/null; }; then function perform_remediation { # the mount point /home has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/home")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/home' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /home in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /home)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|usrquota)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /home defaults,${previous_mount_opts}usrquota 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "usrquota"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,usrquota|" /etc/fstab fi if mkdir -p "/home"; then if mountpoint -q "/home"; then mount -o remount --target "/home" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_usrquota - no_reboot_needed - name: 'Add usrquota Option to /home: Check information associated to mountpoint' command: findmnt --fstab '/home' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_usrquota - no_reboot_needed - name: 'Add usrquota Option to /home: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_usrquota - no_reboot_needed - name: 'Add usrquota Option to /home: If /home not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /home - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_usrquota - no_reboot_needed - name: 'Add usrquota Option to /home: Make sure usrquota option is part of the to /home options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',usrquota'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "usrquota" not in mount_info.options tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_usrquota - no_reboot_needed - name: 'Add usrquota Option to /home: Ensure /home is mounted with usrquota option' mount: path: /home src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/home" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-CM-6(b) - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_home_usrquota - no_reboot_needed part /home --mountoptions="usrquota" Add nodev Option to Non-Root Local Partitions The nodev mount option prevents files from being interpreted as character or block devices. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of any non-root local partitions. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-3 SRG-OS-000368-GPOS-00154 SRG-OS-000480-GPOS-00227 R28 The nodev mount option prevents files from being interpreted as character or block devices. The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails, for which it is not advised to set nodev on these filesystems. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then MOUNT_OPTION="nodev" # Create array of local non-root partitions readarray -t partitions_records < <(findmnt --mtab --raw --evaluate | grep "^/\w" | grep -v "^/proc" | grep "\s/dev/\w") # Create array of polyinstantiated directories, in case one of them is found in mtab readarray -t polyinstantiated_dirs < \ <(grep -oP "^\s*[^#\s]+\s+\S+" /etc/security/namespace.conf | grep -oP "(?<=\s)\S+?(?=/?\$)") # Define excluded non-local file systems excluded_fstypes=( afs autofs ceph cifs smb3 smbfs sshfs ncpfs ncp nfs nfs4 gfs gfs2 glusterfs gpfs pvfs2 ocfs2 lustre davfs fuse.sshfs ) for partition_record in "${partitions_records[@]}"; do # Get all important information for fstab mount_point="$(echo "${partition_record}" | cut -d " " -f1)" device="$(echo "${partition_record}" | cut -d " " -f2)" device_type="$(echo "${partition_record}" | cut -d " " -f3)" # Skip polyinstantiated directories if printf '%s\0' "${polyinstantiated_dirs[@]}" | grep -qxzF "$mount_point"; then continue fi # Skip any non-local filesystem for excluded_fstype in "${excluded_fstypes[@]}"; do if [[ "$device_type" == "$excluded_fstype" ]]; then # jump out of both loops and move to next partition_record continue 2 fi done # If we reach here, it's a local, non-root partition that isn't excluded. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" $mount_point)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|$MOUNT_OPTION)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="$device_type" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo "$device $mount_point $device_type defaults,${previous_mount_opts}$MOUNT_OPTION 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "$MOUNT_OPTION"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,$MOUNT_OPTION|" /etc/fstab fi if mkdir -p "$mount_point"; then if mountpoint -q "$mount_point"; then mount -o remount --target "$mount_point" fi fi done # Remediate unmounted /etc/fstab entries sed -i -E '/nodev/! s;^\s*(/dev/\S+|UUID=\S+)\s+(/\w\S*)\s+(\S+)\s+(\S+)(.*)$;\1 \2 \3 \4,nodev \5;' /etc/fstab else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nodev_nonroot_local_partitions - no_reboot_needed - name: 'Add nodev Option to Non-Root Local Partitions: Refresh facts' ansible.builtin.setup: gather_subset: mounts when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nodev_nonroot_local_partitions - no_reboot_needed - name: 'Add nodev Option to Non-Root Local Partitions: Define excluded (non-local) file systems' ansible.builtin.set_fact: excluded_fstypes: - afs - autofs - ceph - cifs - smb3 - smbfs - sshfs - ncpfs - ncp - nfs - nfs4 - gfs - gfs2 - glusterfs - gpfs - pvfs2 - ocfs2 - lustre - davfs - fuse.sshfs when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nodev_nonroot_local_partitions - no_reboot_needed - name: 'Add nodev Option to Non-Root Local Partitions: Ensure non-root local partitions are mounted with nodev option' ansible.posix.mount: path: '{{ item.mount }}' src: '{{ item.device }}' opts: '{{ item.options }},nodev' state: mounted fstype: '{{ item.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - item.mount is match('/\w') - item.options is not search('nodev') - item.fstype not in excluded_fstypes with_items: - '{{ ansible_facts.mounts }}' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nodev_nonroot_local_partitions - no_reboot_needed - name: 'Add nodev Option to Non-Root Local Partitions: Ensure non-root local partitions are present with nodev option in /etc/fstab' ansible.builtin.replace: path: /etc/fstab regexp: ^\s*(?!#)(/dev/\S+|UUID=\S+)\s+(/\w\S*)\s+(\S+)\s+(?!.*\bnodev\b)(\S+)(.*)$ replace: \1 \2 \3 \4,nodev \5 when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nodev_nonroot_local_partitions - no_reboot_needed Add nodev Option to Removable Media Partitions The nodev mount option prevents files from being interpreted as character or block devices. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of any removable media partitions. 11 12 13 14 16 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.06 DSS05.07 DSS06.03 DSS06.06 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.7.1.1 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 A.9.2.1 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.AC-3 PR.AC-6 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000480-GPOS-00227 The only legitimate location for device files is the /dev directory located on the root partition. An exception to this is chroot jails, and it is not advised to set nodev on partitions which contain their root filesystems. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then var_removable_partition='' device_regex="^\s*$var_removable_partition\s\+" mount_option="nodev" if grep -q $device_regex /etc/fstab ; then previous_opts=$(grep $device_regex /etc/fstab | awk '{print $4}') sed -i "s|\($device_regex.*$previous_opts\)|\1,$mount_option|" /etc/fstab else echo "Not remediating, because there is no record of $var_removable_partition in /etc/fstab" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nodev_removable_partitions - no_reboot_needed - name: XCCDF Value var_removable_partition # promote to variable set_fact: var_removable_partition: !!str tags: - always - name: Ensure permission nodev are set on var_removable_partition ansible.builtin.lineinfile: path: /etc/fstab regexp: ^\s*({{ var_removable_partition }})\s+([^\s]*)\s+([^\s]*)\s+([^\s]*)(.*)$ backrefs: true line: \1 \2 \3 \4,nodev \5 when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nodev_removable_partitions - no_reboot_needed Add noexec Option to Removable Media Partitions The noexec mount option prevents the direct execution of binaries on the mounted filesystem. Preventing the direct execution of binaries from removable media (such as a USB key) provides a defense against malicious software that may be present on such untrusted media. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of any removable media partitions. 11 12 13 14 16 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.06 DSS05.07 DSS06.03 DSS06.06 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.7.1.1 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 A.9.2.1 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.AC-3 PR.AC-6 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000480-GPOS-00227 Allowing users to execute binaries from removable media such as USB keys exposes the system to potential compromise. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then var_removable_partition='' device_regex="^\s*$var_removable_partition\s\+" mount_option="noexec" if grep -q $device_regex /etc/fstab ; then previous_opts=$(grep $device_regex /etc/fstab | awk '{print $4}') sed -i "s|\($device_regex.*$previous_opts\)|\1,$mount_option|" /etc/fstab else echo "Not remediating, because there is no record of $var_removable_partition in /etc/fstab" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_noexec_removable_partitions - no_reboot_needed - name: XCCDF Value var_removable_partition # promote to variable set_fact: var_removable_partition: !!str tags: - always - name: Ensure permission noexec are set on var_removable_partition ansible.builtin.lineinfile: path: /etc/fstab regexp: ^\s*({{ var_removable_partition }})\s+([^\s]*)\s+([^\s]*)\s+([^\s]*)(.*)$ backrefs: true line: \1 \2 \3 \4,noexec \5 when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_noexec_removable_partitions - no_reboot_needed Add nosuid Option to Removable Media Partitions The nosuid mount option prevents set-user-identifier (SUID) and set-group-identifier (SGID) permissions from taking effect. These permissions allow users to execute binaries with the same permissions as the owner and group of the file respectively. Users should not be allowed to introduce SUID and SGID files into the system via partitions mounted from removeable media. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of any removable media partitions. 11 12 13 14 15 16 18 3 5 8 9 APO01.06 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.06 DSS05.07 DSS06.02 DSS06.03 DSS06.06 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 5.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.11.2.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.AC-3 PR.AC-4 PR.AC-6 PR.DS-5 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000480-GPOS-00227 The presence of SUID and SGID executables should be tightly controlled. Allowing users to introduce SUID or SGID binaries from partitions mounted off of removable media would allow them to introduce their own highly-privileged programs. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then var_removable_partition='' device_regex="^\s*$var_removable_partition\s\+" mount_option="nosuid" if grep -q $device_regex /etc/fstab ; then previous_opts=$(grep $device_regex /etc/fstab | awk '{print $4}') sed -i "s|\($device_regex.*$previous_opts\)|\1,$mount_option|" /etc/fstab else echo "Not remediating, because there is no record of $var_removable_partition in /etc/fstab" >&2 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nosuid_removable_partitions - no_reboot_needed - name: XCCDF Value var_removable_partition # promote to variable set_fact: var_removable_partition: !!str tags: - always - name: Ensure permission nosuid are set on var_removable_partition ansible.builtin.lineinfile: path: /etc/fstab regexp: ^\s*({{ var_removable_partition }})\s+([^\s]*)\s+([^\s]*)\s+([^\s]*)(.*)$ backrefs: true line: \1 \2 \3 \4,nosuid \5 when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_nosuid_removable_partitions - no_reboot_needed Add nosuid Option to /opt The nosuid mount option can be used to prevent execution of setuid programs in /opt. The SUID and SGID permissions should not be required in this directory. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /opt. R28 The presence of SUID and SGID executables should be tightly controlled. The /opt directory contains additional software packages. Users should not be able to execute SUID or SGID binaries from this directory. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/opt" > /dev/null || findmnt --fstab "/opt" > /dev/null; }; then function perform_remediation { # the mount point /opt has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/opt")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/opt' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /opt in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /opt)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /opt defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/opt"; then if mountpoint -q "/opt"; then mount -o remount --target "/opt" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_opt_nosuid - no_reboot_needed - name: 'Add nosuid Option to /opt: Check information associated to mountpoint' command: findmnt --fstab '/opt' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/opt" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_opt_nosuid - no_reboot_needed - name: 'Add nosuid Option to /opt: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/opt" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_opt_nosuid - no_reboot_needed - name: 'Add nosuid Option to /opt: If /opt not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /opt - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/opt" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_opt_nosuid - no_reboot_needed - name: 'Add nosuid Option to /opt: Make sure nosuid option is part of the to /opt options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/opt" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_opt_nosuid - no_reboot_needed - name: 'Add nosuid Option to /opt: Ensure /opt is mounted with nosuid option' mount: path: /opt src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/opt" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_opt_nosuid - no_reboot_needed part /opt --mountoptions="nosuid" Add hidepid Option to /proc The hidepid mount option is applicable to /proc and is used to control who can access the information in /proc/[pid] directories. The option can have one of the following values: 0: Everybody may access all /proc/[pid] directories. 1: Users may not access files and subdirectories inside any /proc/[pid] directories but their own. The /proc/[pid] directories themselves remain visible. 2: Same as for mode 1, but in addition the /proc/[pid] directories belonging to other users become invisible. For example, if you choose the value 2: Add the hidepid=2 option to the fourth column of /etc/fstab for the line which controls mounting of /proc. Hiding the pid of processes may lead to problems with PolicyKit and D-Bus, it may also convey a false sense of security. Users should not be able to see and access directories within /proc, which are not related to their own processes in a system. Otherwise, sensitive information from other users could be seem. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then function perform_remediation { var_mount_option_proc_hidepid='' mountoption="hidepid=$var_mount_option_proc_hidepid" mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /proc)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|$mountoption)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="proc" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo "proc /proc proc defaults,${previous_mount_opts}$mountoption 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "$mountoption"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,$mountoption|" /etc/fstab fi if mkdir -p "/proc"; then if mountpoint -q "/proc"; then mount -o remount --target "/proc" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - low_severity - mount_option_proc_hidepid - no_reboot_needed - name: XCCDF Value var_mount_option_proc_hidepid # promote to variable set_fact: var_mount_option_proc_hidepid: !!str tags: - always - name: 'Add hidepid Option to /proc: Check information associated to mountpoint' command: findmnt '/proc' register: device_name failed_when: device_name.rc > 1 changed_when: false when: ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) tags: - configure_strategy - high_disruption - low_complexity - low_severity - mount_option_proc_hidepid - no_reboot_needed - name: 'Add hidepid Option to /proc: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - low_severity - mount_option_proc_hidepid - no_reboot_needed - name: 'Add hidepid Option to /proc: If /proc not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /proc - proc - proc - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - ("" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - low_severity - mount_option_proc_hidepid - no_reboot_needed - name: 'Add hidepid Option to /proc: Make sure hidepid option is part of the to /proc options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',hidepid=''~var_mount_option_proc_hidepid~'''' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined and "hidepid" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - low_severity - mount_option_proc_hidepid - no_reboot_needed - name: 'Add hidepid Option to /proc: Ensure /proc is mounted with hidepid option' mount: path: /proc src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - low_severity - mount_option_proc_hidepid - no_reboot_needed Add nosuid Option to /srv The nosuid mount option can be used to prevent execution of setuid programs in /srv. The SUID and SGID permissions should not be required in this directory. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /srv. R28 The presence of SUID and SGID executables should be tightly controlled. The /srv directory contains files served by various network services such as FTP. Users should not be able to execute SUID or SGID binaries from this directory. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/srv" > /dev/null || findmnt --fstab "/srv" > /dev/null; }; then function perform_remediation { # the mount point /srv has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/srv")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/srv' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /srv in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /srv)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /srv defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/srv"; then if mountpoint -q "/srv"; then mount -o remount --target "/srv" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_srv_nosuid - no_reboot_needed - name: 'Add nosuid Option to /srv: Check information associated to mountpoint' command: findmnt --fstab '/srv' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/srv" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_srv_nosuid - no_reboot_needed - name: 'Add nosuid Option to /srv: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/srv" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_srv_nosuid - no_reboot_needed - name: 'Add nosuid Option to /srv: If /srv not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /srv - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/srv" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_srv_nosuid - no_reboot_needed - name: 'Add nosuid Option to /srv: Make sure nosuid option is part of the to /srv options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/srv" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_srv_nosuid - no_reboot_needed - name: 'Add nosuid Option to /srv: Ensure /srv is mounted with nosuid option' mount: path: /srv src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/srv" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_srv_nosuid - no_reboot_needed part /srv --mountoptions="nosuid" Add nodev Option to /tmp The nodev mount option can be used to prevent device files from being created in /tmp. Legitimate character and block devices should not exist within temporary directories like /tmp. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /tmp. 11 13 14 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS05.06 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 1.1.2.1.2 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/tmp" > /dev/null || findmnt --fstab "/tmp" > /dev/null; }; then function perform_remediation { # the mount point /tmp has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/tmp")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /tmp in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /tmp)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /tmp defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/tmp"; then if mountpoint -q "/tmp"; then mount -o remount --target "/tmp" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /tmp: Check information associated to mountpoint' command: findmnt --fstab '/tmp' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /tmp: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /tmp: If /tmp not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /tmp - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /tmp: Make sure nodev option is part of the to /tmp options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nodev" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /tmp: Ensure /tmp is mounted with nodev option' mount: path: /tmp src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nodev - no_reboot_needed part /tmp --mountoptions="nodev" Add noexec Option to /tmp The noexec mount option can be used to prevent binaries from being executed out of /tmp. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /tmp. 11 13 14 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS05.06 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 R28 1.1.2.1.4 Allowing users to execute binaries from world-writable directories such as /tmp should never be necessary in normal operation and can expose the system to potential compromise. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/tmp" > /dev/null || findmnt --fstab "/tmp" > /dev/null; }; then function perform_remediation { # the mount point /tmp has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/tmp")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /tmp in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /tmp)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /tmp defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/tmp"; then if mountpoint -q "/tmp"; then mount -o remount --target "/tmp" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /tmp: Check information associated to mountpoint' command: findmnt --fstab '/tmp' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /tmp: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /tmp: If /tmp not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /tmp - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /tmp: Make sure noexec option is part of the to /tmp options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "noexec" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /tmp: Ensure /tmp is mounted with noexec option' mount: path: /tmp src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_noexec - no_reboot_needed part /tmp --mountoptions="noexec" Add nosuid Option to /tmp The nosuid mount option can be used to prevent execution of setuid programs in /tmp. The SUID and SGID permissions should not be required in these world-writable directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /tmp. 11 13 14 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS05.06 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.11.2.9 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.8.2.1 A.8.2.2 A.8.2.3 A.8.3.1 A.8.3.3 A.9.1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 R28 1.1.2.1.3 The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from temporary storage partitions. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/tmp" > /dev/null || findmnt --fstab "/tmp" > /dev/null; }; then function perform_remediation { # the mount point /tmp has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/tmp")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /tmp in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /tmp)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /tmp defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/tmp"; then if mountpoint -q "/tmp"; then mount -o remount --target "/tmp" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /tmp: Check information associated to mountpoint' command: findmnt --fstab '/tmp' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /tmp: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /tmp: If /tmp not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /tmp - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /tmp: Make sure nosuid option is part of the to /tmp options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /tmp: Ensure /tmp is mounted with nosuid option' mount: path: /tmp src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_tmp_nosuid - no_reboot_needed part /tmp --mountoptions="nosuid" Add nodev Option to /var/log/audit The nodev mount option can be used to prevent device files from being created in /var/log/audit. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /var/log/audit. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 FMT_SMF_EXT.1 SRG-OS-000368-GPOS-00154 1.1.2.7.2 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/log/audit" > /dev/null || findmnt --fstab "/var/log/audit" > /dev/null; }; then function perform_remediation { # the mount point /var/log/audit has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/log/audit")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/log/audit' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/log/audit in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/log/audit)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/log/audit defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/var/log/audit"; then if mountpoint -q "/var/log/audit"; then mount -o remount --target "/var/log/audit" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log/audit: Check information associated to mountpoint' command: findmnt --fstab '/var/log/audit' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log/audit: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log/audit: If /var/log/audit not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/log/audit - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log/audit: Make sure nodev option is part of the to /var/log/audit options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nodev" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log/audit: Ensure /var/log/audit is mounted with nodev option' mount: path: /var/log/audit src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nodev - no_reboot_needed part /var/log/audit --mountoptions="nodev" Add noexec Option to /var/log/audit The noexec mount option can be used to prevent binaries from being executed out of /var/log/audit. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /var/log/audit. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 FMT_SMF_EXT.1 SRG-OS-000368-GPOS-00154 1.1.2.7.4 Allowing users to execute binaries from directories containing audit log files such as /var/log/audit should never be necessary in normal operation and can expose the system to potential compromise. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/log/audit" > /dev/null || findmnt --fstab "/var/log/audit" > /dev/null; }; then function perform_remediation { # the mount point /var/log/audit has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/log/audit")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/log/audit' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/log/audit in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/log/audit)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/log/audit defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/var/log/audit"; then if mountpoint -q "/var/log/audit"; then mount -o remount --target "/var/log/audit" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log/audit: Check information associated to mountpoint' command: findmnt --fstab '/var/log/audit' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log/audit: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log/audit: If /var/log/audit not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/log/audit - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log/audit: Make sure noexec option is part of the to /var/log/audit options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "noexec" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log/audit: Ensure /var/log/audit is mounted with noexec option' mount: path: /var/log/audit src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_noexec - no_reboot_needed part /var/log/audit --mountoptions="noexec" Add nosuid Option to /var/log/audit The nosuid mount option can be used to prevent execution of setuid programs in /var/log/audit. The SUID and SGID permissions should not be required in directories containing audit log files. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /var/log/audit. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 FMT_SMF_EXT.1 SRG-OS-000368-GPOS-00154 1.1.2.7.3 The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from partitions designated for audit log files. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/log/audit" > /dev/null || findmnt --fstab "/var/log/audit" > /dev/null; }; then function perform_remediation { # the mount point /var/log/audit has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/log/audit")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/log/audit' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/log/audit in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/log/audit)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/log/audit defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/var/log/audit"; then if mountpoint -q "/var/log/audit"; then mount -o remount --target "/var/log/audit" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log/audit: Check information associated to mountpoint' command: findmnt --fstab '/var/log/audit' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log/audit: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log/audit: If /var/log/audit not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/log/audit - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log/audit: Make sure nosuid option is part of the to /var/log/audit options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log/audit: Ensure /var/log/audit is mounted with nosuid option' mount: path: /var/log/audit src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log/audit" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_audit_nosuid - no_reboot_needed part /var/log/audit --mountoptions="nosuid" Add nodev Option to /var/log The nodev mount option can be used to prevent device files from being created in /var/log. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /var/log. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 1.1.2.6.2 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/log" > /dev/null || findmnt --fstab "/var/log" > /dev/null; }; then function perform_remediation { # the mount point /var/log has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/log")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/log' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/log in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/log)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/log defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/var/log"; then if mountpoint -q "/var/log"; then mount -o remount --target "/var/log" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log: Check information associated to mountpoint' command: findmnt --fstab '/var/log' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log: If /var/log not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/log - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log: Make sure nodev option is part of the to /var/log options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nodev" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nodev - no_reboot_needed - name: 'Add nodev Option to /var/log: Ensure /var/log is mounted with nodev option' mount: path: /var/log src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nodev - no_reboot_needed part /var/log --mountoptions="nodev" Add noexec Option to /var/log The noexec mount option can be used to prevent binaries from being executed out of /var/log. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /var/log. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 R28 1.1.2.6.4 Allowing users to execute binaries from directories containing log files such as /var/log should never be necessary in normal operation and can expose the system to potential compromise. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/log" > /dev/null || findmnt --fstab "/var/log" > /dev/null; }; then function perform_remediation { # the mount point /var/log has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/log")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/log' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/log in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/log)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/log defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/var/log"; then if mountpoint -q "/var/log"; then mount -o remount --target "/var/log" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log: Check information associated to mountpoint' command: findmnt --fstab '/var/log' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log: If /var/log not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/log - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log: Make sure noexec option is part of the to /var/log options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "noexec" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_noexec - no_reboot_needed - name: 'Add noexec Option to /var/log: Ensure /var/log is mounted with noexec option' mount: path: /var/log src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_noexec - no_reboot_needed part /var/log --mountoptions="noexec" Add nosuid Option to /var/log The nosuid mount option can be used to prevent execution of setuid programs in /var/log. The SUID and SGID permissions should not be required in directories containing log files. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /var/log. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 R28 1.1.2.6.3 The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from partitions designated for log files. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/log" > /dev/null || findmnt --fstab "/var/log" > /dev/null; }; then function perform_remediation { # the mount point /var/log has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/log")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/log' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/log in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/log)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/log defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/var/log"; then if mountpoint -q "/var/log"; then mount -o remount --target "/var/log" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log: Check information associated to mountpoint' command: findmnt --fstab '/var/log' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log: If /var/log not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/log - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log: Make sure nosuid option is part of the to /var/log options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/log: Ensure /var/log is mounted with nosuid option' mount: path: /var/log src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/log" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_log_nosuid - no_reboot_needed part /var/log --mountoptions="nosuid" Add nodev Option to /var The nodev mount option can be used to prevent device files from being created in /var. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /var. CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-2 PR.PT-3 SRG-OS-000368-GPOS-00154 1.1.2.4.2 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var" > /dev/null || findmnt --fstab "/var" > /dev/null; }; then function perform_remediation { # the mount point /var has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/var"; then if mountpoint -q "/var"; then mount -o remount --target "/var" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nodev - no_reboot_needed - name: 'Add nodev Option to /var: Check information associated to mountpoint' command: findmnt --fstab '/var' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nodev - no_reboot_needed - name: 'Add nodev Option to /var: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nodev - no_reboot_needed - name: 'Add nodev Option to /var: If /var not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nodev - no_reboot_needed - name: 'Add nodev Option to /var: Make sure nodev option is part of the to /var options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nodev" not in mount_info.options tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nodev - no_reboot_needed - name: 'Add nodev Option to /var: Ensure /var is mounted with nodev option' mount: path: /var src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - NIST-800-53-AC-6 - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-MP-7 - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nodev - no_reboot_needed part /var --mountoptions="nodev" Add noexec Option to /var The noexec mount option can be used to prevent binaries from being executed out of /var. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /var. R28 The /var directory contains variable system data such as logs, mails and caches. No binaries should be executed from this directory. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var" > /dev/null || findmnt --fstab "/var" > /dev/null; }; then function perform_remediation { # the mount point /var has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/var"; then if mountpoint -q "/var"; then mount -o remount --target "/var" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_noexec - no_reboot_needed - name: 'Add noexec Option to /var: Check information associated to mountpoint' command: findmnt --fstab '/var' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_noexec - no_reboot_needed - name: 'Add noexec Option to /var: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_noexec - no_reboot_needed - name: 'Add noexec Option to /var: If /var not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_noexec - no_reboot_needed - name: 'Add noexec Option to /var: Make sure noexec option is part of the to /var options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "noexec" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_noexec - no_reboot_needed - name: 'Add noexec Option to /var: Ensure /var is mounted with noexec option' mount: path: /var src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_noexec - no_reboot_needed part /var --mountoptions="noexec" Add nosuid Option to /var The nosuid mount option can be used to prevent execution of setuid programs in /var. The SUID and SGID permissions should not be required for this directory. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /var. R28 1.1.2.4.3 The presence of SUID and SGID executables should be tightly controlled. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var" > /dev/null || findmnt --fstab "/var" > /dev/null; }; then function perform_remediation { # the mount point /var has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/var"; then if mountpoint -q "/var"; then mount -o remount --target "/var" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var: Check information associated to mountpoint' command: findmnt --fstab '/var' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var: If /var not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var: Make sure nosuid option is part of the to /var options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var: Ensure /var is mounted with nosuid option' mount: path: /var src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_nosuid - no_reboot_needed part /var --mountoptions="nosuid" Bind Mount /var/tmp To /tmp The /var/tmp directory is a world-writable directory. Bind-mount it to /tmp in order to consolidate temporary storage into one location protected by the same techniques as /tmp. To do so, edit /etc/fstab and add the following line: /tmp /var/tmp none rw,nodev,noexec,nosuid,bind 0 0 See the mount(8) man page for further explanation of bind mounting. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) AC-6 AC-6(1) MP-7 PR.IP-1 PR.PT-3 Having multiple locations for temporary storage is not required. Unless absolutely necessary to meet requirements, the storage location /var/tmp should be bind mounted to /tmp and thus share the same protections. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/tmp" > /dev/null || findmnt --fstab "/var/tmp" > /dev/null; }; then # Delete particular /etc/fstab's row if /var/tmp is already configured to # represent a mount point (for some device or filesystem other than /tmp) if grep -q -P '.*\/var\/tmp.*' /etc/fstab then sed -i '/.*\/var\/tmp.*/d' /etc/fstab fi umount /var/tmp # Bind-mount /var/tmp to /tmp via /etc/fstab (preserving the /etc/fstab form) printf "%-24s%-24s%-8s%-32s%-3s\n" "/tmp" "/var/tmp" "none" "rw,nodev,noexec,nosuid,bind" "0 0" >> /etc/fstab mkdir -p /var/tmp mount -B /tmp /var/tmp else >&2 echo 'Remediation is not applicable, nothing was done' fi Add nodev Option to /var/tmp The nodev mount option can be used to prevent device files from being created in /var/tmp. Legitimate character and block devices should not exist within temporary directories like /var/tmp. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /var/tmp. SRG-OS-000368-GPOS-00154 1.1.2.5.2 The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/tmp" > /dev/null || findmnt --fstab "/var/tmp" > /dev/null; }; then function perform_remediation { # the mount point /var/tmp has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/tmp")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/tmp in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/tmp)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/tmp defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab fi if mkdir -p "/var/tmp"; then if mountpoint -q "/var/tmp"; then mount -o remount --target "/var/tmp" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /var/tmp: Check information associated to mountpoint' command: findmnt --fstab '/var/tmp' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /var/tmp: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /var/tmp: If /var/tmp not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/tmp - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /var/tmp: Make sure nodev option is part of the to /var/tmp options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nodev" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nodev - no_reboot_needed - name: 'Add nodev Option to /var/tmp: Ensure /var/tmp is mounted with nodev option' mount: path: /var/tmp src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nodev - no_reboot_needed part /var/tmp --mountoptions="nodev" Add noexec Option to /var/tmp The noexec mount option can be used to prevent binaries from being executed out of /var/tmp. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /var/tmp. SRG-OS-000368-GPOS-00154 R28 1.1.2.5.4 Allowing users to execute binaries from world-writable directories such as /var/tmp should never be necessary in normal operation and can expose the system to potential compromise. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/tmp" > /dev/null || findmnt --fstab "/var/tmp" > /dev/null; }; then function perform_remediation { # the mount point /var/tmp has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/tmp")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/tmp in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/tmp)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/tmp defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab fi if mkdir -p "/var/tmp"; then if mountpoint -q "/var/tmp"; then mount -o remount --target "/var/tmp" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /var/tmp: Check information associated to mountpoint' command: findmnt --fstab '/var/tmp' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /var/tmp: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /var/tmp: If /var/tmp not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/tmp - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /var/tmp: Make sure noexec option is part of the to /var/tmp options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "noexec" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_noexec - no_reboot_needed - name: 'Add noexec Option to /var/tmp: Ensure /var/tmp is mounted with noexec option' mount: path: /var/tmp src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_noexec - no_reboot_needed part /var/tmp --mountoptions="noexec" Add nosuid Option to /var/tmp The nosuid mount option can be used to prevent execution of setuid programs in /var/tmp. The SUID and SGID permissions should not be required in these world-writable directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /var/tmp. SRG-OS-000368-GPOS-00154 R28 1.1.2.5.3 The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from temporary storage partitions. # Remediation is applicable only in certain platforms if ( ! ( { rpm --quiet -q kernel ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { findmnt --kernel "/var/tmp" > /dev/null || findmnt --fstab "/var/tmp" > /dev/null; }; then function perform_remediation { # the mount point /var/tmp has to be defined in /etc/fstab # before this remediation can be executed. In case it is not defined, the # remediation aborts and no changes regarding the mount point are done. mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/var/tmp")" grep "$mount_point_match_regexp" -q /etc/fstab \ || { echo "The mount point '/var/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2; echo "Not remediating, because there is no record of /var/tmp in /etc/fstab" >&2; return 1; } mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /var/tmp)" # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab if ! grep -q "$mount_point_match_regexp" /etc/fstab; then # runtime opts without some automatic kernel/userspace-added defaults previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 | awk '{print $4}' \ | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//") [ "$previous_mount_opts" ] && previous_mount_opts+="," # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in # fstab as "block". The next variable is to satisfy shellcheck SC2050. fs_type="" if [ "$fs_type" == "iso9660" ] ; then previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts") fi echo " /var/tmp defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}') sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab fi if mkdir -p "/var/tmp"; then if mountpoint -q "/var/tmp"; then mount -o remount --target "/var/tmp" fi fi } perform_remediation else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/tmp: Check information associated to mountpoint' command: findmnt --fstab '/var/tmp' register: device_name failed_when: device_name.rc > 1 changed_when: false when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/tmp: Create mount_info dictionary variable' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - '{{ device_name.stdout_lines[0].split() | list | lower }}' - '{{ device_name.stdout_lines[1].split() | list }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length > 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/tmp: If /var/tmp not mounted, craft mount_info manually' set_fact: mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}' with_together: - - target - source - fstype - options - - /var/tmp - '' - '' - defaults when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - ("--fstab" | length == 0) - device_name.stdout is defined and device_name.stdout_lines is defined - (device_name.stdout | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/tmp: Make sure nosuid option is part of the to /var/tmp options' set_fact: mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid'' }) }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined and "nosuid" not in mount_info.options tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nosuid - no_reboot_needed - name: 'Add nosuid Option to /var/tmp: Ensure /var/tmp is mounted with nosuid option' mount: path: /var/tmp src: '{{ mount_info.source }}' opts: '{{ mount_info.options }}' state: mounted fstype: '{{ mount_info.fstype }}' when: - ( not ( "kernel" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages ) and not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"] ) ) - '"/var/tmp" in ansible_mounts | map(attribute="mount") | list' - mount_info is defined - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab" | length == 0) tags: - configure_strategy - high_disruption - low_complexity - medium_severity - mount_option_var_tmp_nosuid - no_reboot_needed part /var/tmp --mountoptions="nosuid" Restrict Programs from Dangerous Execution Patterns The recommendations in this section are designed to ensure that the system's features to protect against potentially dangerous program execution are activated. These protections are applied at the system initialization or kernel level, and defend against certain types of badly-configured or compromised programs. Disable the uvcvideo module If the device contains a camera it should be covered or disabled when not in use. CM-7 (a) CM-7 (5) (b) SRG-OS-000095-GPOS-00049 SRG-OS-000370-GPOS-00155 Failing to disconnect from collaborative computing devices (i.e., cameras) can result in subsequent compromises of organizational information. Providing easy methods to physically disconnect from such devices after a collaborative computing session helps to ensure participants actually carry out the disconnect activity without having to go through complex and tedious procedures. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if LC_ALL=C grep -q -m 1 "^install uvcvideo" /etc/modprobe.d/uvcvideo.conf ; then sed -i 's#^install uvcvideo.*#install uvcvideo /bin/false#g' /etc/modprobe.d/uvcvideo.conf else echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/uvcvideo.conf echo "install uvcvideo /bin/false" >> /etc/modprobe.d/uvcvideo.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-7 (5) (b) - NIST-800-53-CM-7 (a) - disable_strategy - kernel_module_uvcvideo_disabled - low_complexity - medium_disruption - medium_severity - reboot_required - name: Ensure kernel module 'uvcvideo' is disabled ansible.builtin.lineinfile: create: true dest: /etc/modprobe.d/uvcvideo.conf regexp: install\s+uvcvideo line: install uvcvideo /bin/false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-7 (5) (b) - NIST-800-53-CM-7 (a) - disable_strategy - kernel_module_uvcvideo_disabled - low_complexity - medium_disruption - medium_severity - reboot_required --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,install%20uvcvideo%20/bin/false%0Ablacklist%20uvcvideo%0A mode: 0644 path: /etc/modprobe.d/uvcvideo.conf overwrite: true Disable storing core dumps To set the runtime status of the kernel.core_pattern kernel parameter, run the following command: $ sudo sysctl -w kernel.core_pattern=|/bin/false To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.core_pattern = |/bin/false SC-7(10) SRG-OS-000480-GPOS-00227 3.3.1.1 3.3.1 3.3 A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers trying to debug problems. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.core_pattern from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.core_pattern.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.core_pattern" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.core_pattern # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.core_pattern="|/bin/false" fi # # If kernel.core_pattern present in /etc/sysctl.conf, change value to "|/bin/false" # else, add "kernel.core_pattern = |/bin/false" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.core_pattern") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "|/bin/false" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.core_pattern\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.core_pattern\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_pattern - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.core_pattern.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_pattern - name: Comment out any occurrences of kernel.core_pattern from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.core_pattern replace: '#kernel.core_pattern' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_pattern - name: Ensure sysctl kernel.core_pattern is set to |/bin/false ansible.posix.sysctl: name: kernel.core_pattern value: '|/bin/false' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_pattern --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.core_pattern%20%3D%20%7C/bin/false%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_core_pattern.conf overwrite: true Configure file name of core dumps To set the runtime status of the kernel.core_uses_pid kernel parameter, run the following command: $ sudo sysctl -w kernel.core_uses_pid=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.core_uses_pid = 0 FMT_SMF_EXT.1 The default coredump filename is core. By setting core_uses_pid to 1, the coredump filename becomes core.PID. If core_pattern does not include %p (default does not) and core_uses_pid is set, then .PID will be appended to the filename. When combined with kernel.core_pattern = "" configuration, it is ensured that no core dumps are generated and also no confusing error messages are printed by a shell. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.core_uses_pid from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.core_uses_pid.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.core_uses_pid" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.core_uses_pid # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.core_uses_pid="0" fi # # If kernel.core_uses_pid present in /etc/sysctl.conf, change value to "0" # else, add "kernel.core_uses_pid = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.core_uses_pid") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.core_uses_pid\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.core_uses_pid\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_uses_pid - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.core_uses_pid.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_uses_pid - name: Comment out any occurrences of kernel.core_uses_pid from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.core_uses_pid replace: '#kernel.core_uses_pid' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_uses_pid - name: Ensure sysctl kernel.core_uses_pid is set to 0 ansible.posix.sysctl: name: kernel.core_uses_pid value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_core_uses_pid Restrict Access to Kernel Message Buffer To set the runtime status of the kernel.dmesg_restrict kernel parameter, run the following command: $ sudo sysctl -w kernel.dmesg_restrict=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.dmesg_restrict = 1 3.1.5 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) SI-11(a) SI-11(b) FMT_SMF_EXT.1 SRG-OS-000132-GPOS-00067 SRG-OS-000138-GPOS-00069 SRG-APP-000243-CTR-000600 R9 Unprivileged access to the kernel syslog can expose sensitive kernel address information. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.dmesg_restrict from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.dmesg_restrict.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.dmesg_restrict" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.dmesg_restrict # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.dmesg_restrict="1" fi # # If kernel.dmesg_restrict present in /etc/sysctl.conf, change value to "1" # else, add "kernel.dmesg_restrict = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.dmesg_restrict") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.dmesg_restrict\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.dmesg_restrict\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.5 - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_dmesg_restrict - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.dmesg_restrict.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.5 - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_dmesg_restrict - name: Comment out any occurrences of kernel.dmesg_restrict from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.dmesg_restrict replace: '#kernel.dmesg_restrict' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.5 - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_dmesg_restrict - name: Ensure sysctl kernel.dmesg_restrict is set to 1 ansible.posix.sysctl: name: kernel.dmesg_restrict value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.5 - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_dmesg_restrict --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.dmesg_restrict%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_dmesg_restrict.conf overwrite: true Disable Kernel Image Loading To set the runtime status of the kernel.kexec_load_disabled kernel parameter, run the following command: $ sudo sysctl -w kernel.kexec_load_disabled=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.kexec_load_disabled = 1 CM-6 FMT_SMF_EXT.1 SRG-OS-000480-GPOS-00227 SRG-OS-000366-GPOS-00153 Disabling kexec_load allows greater control of the kernel memory. It makes it impossible to load another kernel image after it has been disabled. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.kexec_load_disabled from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.kexec_load_disabled.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.kexec_load_disabled" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.kexec_load_disabled # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.kexec_load_disabled="1" fi # # If kernel.kexec_load_disabled present in /etc/sysctl.conf, change value to "1" # else, add "kernel.kexec_load_disabled = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.kexec_load_disabled") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.kexec_load_disabled\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.kexec_load_disabled\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kexec_load_disabled - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.kexec_load_disabled.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kexec_load_disabled - name: Comment out any occurrences of kernel.kexec_load_disabled from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.kexec_load_disabled replace: '#kernel.kexec_load_disabled' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kexec_load_disabled - name: Ensure sysctl kernel.kexec_load_disabled is set to 1 ansible.posix.sysctl: name: kernel.kexec_load_disabled value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kexec_load_disabled --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.kexec_load_disabled%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_kexec_load_disabled.conf overwrite: true Disable loading and unloading of kernel modules To set the runtime status of the kernel.modules_disabled kernel parameter, run the following command: $ sudo sysctl -w kernel.modules_disabled=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.modules_disabled = 1 This rule doesn't come with remediation. Remediating this rule during the installation process disrupts the install and boot process. R10 Malicious kernel modules can have a significant impact on system security and availability. Disabling loading of kernel modules prevents this threat. Note that once this option has been set, it cannot be reverted without doing a system reboot. Make sure that all needed kernel modules are loaded before setting this option. Kernel panic on oops To set the runtime status of the kernel.panic_on_oops kernel parameter, run the following command: $ sudo sysctl -w kernel.panic_on_oops=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.panic_on_oops = 1 The system may start to panic when it normally wouldn't. A non-catastrophic error that would have allowed the system to continue operating will now result in a panic. R9 An attacker trying to exploit the kernel may trigger kernel OOPSes, panicking the system will impede them from continuing. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.panic_on_oops from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.panic_on_oops.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.panic_on_oops" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.panic_on_oops # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.panic_on_oops="1" fi # # If kernel.panic_on_oops present in /etc/sysctl.conf, change value to "1" # else, add "kernel.panic_on_oops = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.panic_on_oops") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.panic_on_oops\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.panic_on_oops\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_panic_on_oops - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.panic_on_oops.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_panic_on_oops - name: Comment out any occurrences of kernel.panic_on_oops from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.panic_on_oops replace: '#kernel.panic_on_oops' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_panic_on_oops - name: Ensure sysctl kernel.panic_on_oops is set to 1 ansible.posix.sysctl: name: kernel.panic_on_oops value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_panic_on_oops Limit CPU consumption of the Perf system To set the runtime status of the kernel.perf_cpu_time_max_percent kernel parameter, run the following command: $ sudo sysctl -w kernel.perf_cpu_time_max_percent=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.perf_cpu_time_max_percent = 1 R9 The kernel.perf_cpu_time_max_percent configures a treshold of maximum percentile of CPU that can be used by Perf system. Restricting usage of Perf system decreases risk of potential availability problems. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.perf_cpu_time_max_percent from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.perf_cpu_time_max_percent.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.perf_cpu_time_max_percent" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.perf_cpu_time_max_percent # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.perf_cpu_time_max_percent="1" fi # # If kernel.perf_cpu_time_max_percent present in /etc/sysctl.conf, change value to "1" # else, add "kernel.perf_cpu_time_max_percent = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.perf_cpu_time_max_percent") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.perf_cpu_time_max_percent\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.perf_cpu_time_max_percent\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_cpu_time_max_percent - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.perf_cpu_time_max_percent.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_cpu_time_max_percent - name: Comment out any occurrences of kernel.perf_cpu_time_max_percent from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.perf_cpu_time_max_percent replace: '#kernel.perf_cpu_time_max_percent' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_cpu_time_max_percent - name: Ensure sysctl kernel.perf_cpu_time_max_percent is set to 1 ansible.posix.sysctl: name: kernel.perf_cpu_time_max_percent value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_cpu_time_max_percent Limit sampling frequency of the Perf system To set the runtime status of the kernel.perf_event_max_sample_rate kernel parameter, run the following command: $ sudo sysctl -w kernel.perf_event_max_sample_rate=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.perf_event_max_sample_rate = 1 R9 The kernel.perf_event_max_sample_rate parameter configures maximum frequency of collecting of samples for the Perf system. It is expressed in samples per second. Restricting usage of Perf system decreases risk of potential availability problems. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.perf_event_max_sample_rate from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.perf_event_max_sample_rate.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.perf_event_max_sample_rate" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.perf_event_max_sample_rate # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.perf_event_max_sample_rate="1" fi # # If kernel.perf_event_max_sample_rate present in /etc/sysctl.conf, change value to "1" # else, add "kernel.perf_event_max_sample_rate = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.perf_event_max_sample_rate") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.perf_event_max_sample_rate\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.perf_event_max_sample_rate\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_event_max_sample_rate - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.perf_event_max_sample_rate.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_event_max_sample_rate - name: Comment out any occurrences of kernel.perf_event_max_sample_rate from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.perf_event_max_sample_rate replace: '#kernel.perf_event_max_sample_rate' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_event_max_sample_rate - name: Ensure sysctl kernel.perf_event_max_sample_rate is set to 1 ansible.posix.sysctl: name: kernel.perf_event_max_sample_rate value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_perf_event_max_sample_rate Disallow kernel profiling by unprivileged users To set the runtime status of the kernel.perf_event_paranoid kernel parameter, run the following command: $ sudo sysctl -w kernel.perf_event_paranoid=2 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.perf_event_paranoid = 2 AC-6 FMT_SMF_EXT.1 SRG-OS-000132-GPOS-00067 SRG-OS-000138-GPOS-00069 SRG-APP-000243-CTR-000600 R9 Kernel profiling can reveal sensitive information about kernel behaviour. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.perf_event_paranoid from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.perf_event_paranoid.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.perf_event_paranoid" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.perf_event_paranoid # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.perf_event_paranoid="2" fi # # If kernel.perf_event_paranoid present in /etc/sysctl.conf, change value to "2" # else, add "kernel.perf_event_paranoid = 2" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.perf_event_paranoid") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "2" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.perf_event_paranoid\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.perf_event_paranoid\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_perf_event_paranoid - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.perf_event_paranoid.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6 - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_perf_event_paranoid - name: Comment out any occurrences of kernel.perf_event_paranoid from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.perf_event_paranoid replace: '#kernel.perf_event_paranoid' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6 - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_perf_event_paranoid - name: Ensure sysctl kernel.perf_event_paranoid is set to 2 ansible.posix.sysctl: name: kernel.perf_event_paranoid value: '2' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6 - disable_strategy - low_complexity - low_severity - medium_disruption - reboot_required - sysctl_kernel_perf_event_paranoid --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.perf_event_paranoid%3D2%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_perf_event_paranoid.conf overwrite: true Configure maximum number of process identifiers To set the runtime status of the kernel.pid_max kernel parameter, run the following command: $ sudo sysctl -w kernel.pid_max=65536 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.pid_max = 65536 R9 The kernel.pid_max parameter configures upper limit on process identifiers (PID). If this number is not high enough, it might happen that forking of new processes is not possible, because all available PIDs are exhausted. Increasing this number enhances availability. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.pid_max from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.pid_max.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.pid_max" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.pid_max # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.pid_max="65536" fi # # If kernel.pid_max present in /etc/sysctl.conf, change value to "65536" # else, add "kernel.pid_max = 65536" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.pid_max") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "65536" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.pid_max\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.pid_max\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_pid_max - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.pid_max.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_pid_max - name: Comment out any occurrences of kernel.pid_max from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.pid_max replace: '#kernel.pid_max' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_pid_max - name: Ensure sysctl kernel.pid_max is set to 65536 ansible.posix.sysctl: name: kernel.pid_max value: '65536' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_pid_max Disallow magic SysRq key To set the runtime status of the kernel.sysrq kernel parameter, run the following command: $ sudo sysctl -w kernel.sysrq=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.sysrq = 0 R9 The Magic SysRq key allows sending certain commands directly to the running kernel. It can dump various system and process information, potentially revealing sensitive information. It can also reboot or shutdown the machine, disturbing its availability. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.sysrq from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.sysrq.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.sysrq" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.sysrq # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.sysrq="0" fi # # If kernel.sysrq present in /etc/sysctl.conf, change value to "0" # else, add "kernel.sysrq = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.sysrq") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.sysrq\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.sysrq\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_sysrq - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.sysrq.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_sysrq - name: Comment out any occurrences of kernel.sysrq from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.sysrq replace: '#kernel.sysrq' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_sysrq - name: Ensure sysctl kernel.sysrq is set to 0 ansible.posix.sysctl: name: kernel.sysrq value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_sysrq Disable Access to Network bpf() Syscall From Unprivileged Processes To set the runtime status of the kernel.unprivileged_bpf_disabled kernel parameter, run the following command: $ sudo sysctl -w kernel.unprivileged_bpf_disabled=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.unprivileged_bpf_disabled = 1 AC-6 SC-7(10) SRG-OS-000132-GPOS-00067 SRG-OS-000480-GPOS-00227 R9 Loading and accessing the packet filters programs and maps using the bpf() syscall has the potential of revealing sensitive information about the kernel state. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.unprivileged_bpf_disabled from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.unprivileged_bpf_disabled.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.unprivileged_bpf_disabled" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.unprivileged_bpf_disabled # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.unprivileged_bpf_disabled="1" fi # # If kernel.unprivileged_bpf_disabled present in /etc/sysctl.conf, change value to "1" # else, add "kernel.unprivileged_bpf_disabled = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.unprivileged_bpf_disabled") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.unprivileged_bpf_disabled\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.unprivileged_bpf_disabled\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_unprivileged_bpf_disabled - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.unprivileged_bpf_disabled.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_unprivileged_bpf_disabled - name: Comment out any occurrences of kernel.unprivileged_bpf_disabled from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.unprivileged_bpf_disabled replace: '#kernel.unprivileged_bpf_disabled' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_unprivileged_bpf_disabled - name: Ensure sysctl kernel.unprivileged_bpf_disabled is set to 1 ansible.posix.sysctl: name: kernel.unprivileged_bpf_disabled value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_unprivileged_bpf_disabled --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.unprivileged_bpf_disabled%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_unprivileged_bpf_disabled.conf overwrite: true Restrict usage of ptrace to descendant processes To set the runtime status of the kernel.yama.ptrace_scope kernel parameter, run the following command: $ sudo sysctl -w kernel.yama.ptrace_scope=1 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.yama.ptrace_scope = 1 SC-7(10) FMT_SMF_EXT.1 SRG-OS-000132-GPOS-00067 SRG-OS-000480-GPOS-00227 R11 Unrestricted usage of ptrace allows compromised binaries to run ptrace on another processes of the user. Like this, the attacker can steal sensitive information from the target processes (e.g. SSH sessions, web browser, ...) without any additional assistance from the user (i.e. without resorting to phishing). # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.yama.ptrace_scope from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.yama.ptrace_scope.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.yama.ptrace_scope" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.yama.ptrace_scope # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.yama.ptrace_scope="1" fi # # If kernel.yama.ptrace_scope present in /etc/sysctl.conf, change value to "1" # else, add "kernel.yama.ptrace_scope = 1" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.yama.ptrace_scope") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "1" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.yama.ptrace_scope\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.yama.ptrace_scope\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_yama_ptrace_scope - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.yama.ptrace_scope.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_yama_ptrace_scope - name: Comment out any occurrences of kernel.yama.ptrace_scope from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.yama.ptrace_scope replace: '#kernel.yama.ptrace_scope' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_yama_ptrace_scope - name: Ensure sysctl kernel.yama.ptrace_scope is set to 1 ansible.posix.sysctl: name: kernel.yama.ptrace_scope value: '1' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_yama_ptrace_scope --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.yama.ptrace_scope%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_yama_ptrace_scope.conf overwrite: true Harden the operation of the BPF just-in-time compiler To set the runtime status of the net.core.bpf_jit_harden kernel parameter, run the following command: $ sudo sysctl -w net.core.bpf_jit_harden=2 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: net.core.bpf_jit_harden = 2 CM-6 SC-7(10) SRG-OS-000480-GPOS-00227 R12 When hardened, the extended Berkeley Packet Filter just-in-time compiler will randomize any kernel addresses in the BPF programs and maps, and will not expose the JIT addresses in /proc/kallsyms. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of net.core.bpf_jit_harden from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*net.core.bpf_jit_harden.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "net.core.bpf_jit_harden" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for net.core.bpf_jit_harden # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w net.core.bpf_jit_harden="2" fi # # If net.core.bpf_jit_harden present in /etc/sysctl.conf, change value to "2" # else, add "net.core.bpf_jit_harden = 2" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.core.bpf_jit_harden") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "2" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^net.core.bpf_jit_harden\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^net.core.bpf_jit_harden\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_core_bpf_jit_harden - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*net.core.bpf_jit_harden.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_core_bpf_jit_harden - name: Comment out any occurrences of net.core.bpf_jit_harden from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*net.core.bpf_jit_harden replace: '#net.core.bpf_jit_harden' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_core_bpf_jit_harden - name: Ensure sysctl net.core.bpf_jit_harden is set to 2 ansible.posix.sysctl: name: net.core.bpf_jit_harden value: '2' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_net_core_bpf_jit_harden --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,net.core.bpf_jit_harden%3D2%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_net_core_bpf_jit_harden.conf overwrite: true Disable the use of user namespaces To set the runtime status of the user.max_user_namespaces kernel parameter, run the following command: $ sudo sysctl -w user.max_user_namespaces=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: user.max_user_namespaces = 0 When containers are deployed on the machine, the value should be set to large non-zero value. Remediation of this rule might impair or prevent functionality of certain applications. This stands especially for general container usage and for certain desktop applications. There is an alternative rule which performs the same check but it intentionally lacks the remediation part. If needed, you can use the rule sysctl_user_max_user_namespaces_no_remediation. In that case, ensure that such use case is properly documented. SC-39 CM-6(a) FMT_SMF_EXT.1 SRG-OS-000480-GPOS-00227 It is detrimental for operating systems to provide, or install by default, functionality exceeding requirements or system objectives. These unnecessary capabilities or services are often overlooked and therefore may remain unsecured. They increase the risk to the platform by providing additional attack vectors. User namespaces are used primarily for Linux containers. The value 0 disallows the use of user namespaces. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of user.max_user_namespaces from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*user.max_user_namespaces.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "user.max_user_namespaces" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for user.max_user_namespaces # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w user.max_user_namespaces="0" fi # # If user.max_user_namespaces present in /etc/sysctl.conf, change value to "0" # else, add "user.max_user_namespaces = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^user.max_user_namespaces") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^user.max_user_namespaces\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^user.max_user_namespaces\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-39 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_user_max_user_namespaces - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*user.max_user_namespaces.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-39 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_user_max_user_namespaces - name: Comment out any occurrences of user.max_user_namespaces from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*user.max_user_namespaces replace: '#user.max_user_namespaces' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-39 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_user_max_user_namespaces - name: Ensure sysctl user.max_user_namespaces is set to 0 ansible.posix.sysctl: name: user.max_user_namespaces value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-39 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_user_max_user_namespaces --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,user.max_user_namespaces%20%3D%200%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_user_max_user_namespaces.conf overwrite: true Prevent applications from mapping low portion of virtual memory To set the runtime status of the vm.mmap_min_addr kernel parameter, run the following command: $ sudo sysctl -w vm.mmap_min_addr=65536 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: vm.mmap_min_addr = 65536 R8 The vm.mmap_min_addr parameter specifies the minimum virtual address that a process is allowed to mmap. Allowing a process to mmap low portion of virtual memory can have security implications such as such as heightened risk of kernel null pointer dereference defects. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of vm.mmap_min_addr from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*vm.mmap_min_addr.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "vm.mmap_min_addr" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for vm.mmap_min_addr # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w vm.mmap_min_addr="65536" fi # # If vm.mmap_min_addr present in /etc/sysctl.conf, change value to "65536" # else, add "vm.mmap_min_addr = 65536" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^vm.mmap_min_addr") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "65536" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^vm.mmap_min_addr\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^vm.mmap_min_addr\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_vm_mmap_min_addr - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*vm.mmap_min_addr.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_vm_mmap_min_addr - name: Comment out any occurrences of vm.mmap_min_addr from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*vm.mmap_min_addr replace: '#vm.mmap_min_addr' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_vm_mmap_min_addr - name: Ensure sysctl vm.mmap_min_addr is set to 65536 ansible.posix.sysctl: name: vm.mmap_min_addr value: '65536' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_vm_mmap_min_addr Disable Core Dumps A core dump file is the memory image of an executable program when it was terminated by the operating system due to errant behavior. In most cases, only software developers legitimately need to access these files. The core dump files may also contain sensitive information, or unnecessarily occupy large amounts of disk space. Once a hard limit is set in /etc/security/limits.conf, or to a file within the /etc/security/limits.d/ directory, a user cannot increase that limit within his or her own session. If access to core dumps is required, consider restricting them to only certain users or groups. See the limits.conf man page for more information. The core dumps of setuid programs are further protected. The sysctl variable fs.suid_dumpable controls whether the kernel allows core dumps from these programs at all. The default value of 0 is recommended. Disable acquiring, saving, and processing core dumps The systemd-coredump.socket unit is a socket activation of the systemd-coredump@.service which processes core dumps. By masking the unit, core dump processing is disabled. SC-7(10) FMT_SMF_EXT.1 SRG-OS-000480-GPOS-00227 A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers trying to debug problems. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SOCKET_NAME="systemd-coredump.socket" SYSTEMCTL_EXEC='/usr/bin/systemctl' if "$SYSTEMCTL_EXEC" -q list-unit-files --type socket | grep -q "$SOCKET_NAME"; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop "$SOCKET_NAME" fi "$SYSTEMCTL_EXEC" mask "$SOCKET_NAME" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_systemd-coredump_disabled - name: Disable acquiring, saving, and processing core dumps - Collect systemd Socket Units Present in the System ansible.builtin.command: cmd: systemctl -q list-unit-files --type socket register: result_systemd_unit_files changed_when: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_systemd-coredump_disabled - name: Disable acquiring, saving, and processing core dumps - Ensure systemd-coredump.socket is Masked ansible.builtin.systemd: name: systemd-coredump.socket state: stopped enabled: false masked: true when: - '"kernel" in ansible_facts.packages' - result_systemd_unit_files.stdout_lines is search("systemd-coredump.socket") tags: - NIST-800-53-SC-7(10) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_systemd-coredump_disabled Disable core dump backtraces The ProcessSizeMax option in [Coredump] section of /etc/systemd/coredump.conf or in a drop-in file under /etc/systemd/coredump.conf.d/ specifies the maximum size in bytes of a core which will be processed. Core dumps exceeding this size may be stored, but the backtrace will not be generated. If the /etc/systemd/coredump.conf file or a drop-in file under /etc/systemd/coredump.conf.d/ does not already contain the [Coredump] section, the value will not be configured correctly. CM-6 Req-3.2 SRG-OS-000480-GPOS-00227 3.3.1.1 3.3.1 3.3 A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers or system operators trying to debug problems. Enabling core dumps on production systems is not recommended, however there may be overriding operational requirements to enable advanced debuging. Permitting temporary enablement of core dumps during such situations should be reviewed through local needs and policy. # Remediation is applicable only in certain platforms if rpm --quiet -q systemd; then found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/systemd/coredump.conf.d/complianceascode_hardening.conf /etc/systemd/coredump.conf.d/*.conf /etc/systemd/coredump.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[Coredump\]([^\n\[]*\n+)+?[[:space:]]*ProcessSizeMax" "$f"; then sed -i "s/ProcessSizeMax[^(\n)]*/ProcessSizeMax=0/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[Coredump\]" "$f"; then sed -i "/[[:space:]]*\[Coredump\]/a ProcessSizeMax=0" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/systemd/coredump.conf.d/complianceascode_hardening.conf /etc/systemd/coredump.conf.d/*.conf /etc/systemd/coredump.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[Coredump]\nProcessSizeMax=0" >> "$file" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_backtraces - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable core dump backtraces - Search for a section in files ansible.builtin.find: paths: '{{item.path}}' patterns: '{{item.pattern}}' contains: ^\s*\[Coredump\] read_whole_file: true use_regex: true register: systemd_dropin_files_with_section loop: - path: '{{ ''/etc/systemd/coredump.conf'' | dirname }}' pattern: '{{ ''/etc/systemd/coredump.conf'' | basename | regex_escape }}' - path: /etc/systemd/coredump.conf.d pattern: .*\.conf when: '"systemd" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_backtraces - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable core dump backtraces - Count number of files which contain the correct section ansible.builtin.set_fact: count_of_systemd_dropin_files_with_section: '{{systemd_dropin_files_with_section.results | map(attribute=''matched'') | list | map(''int'') | sum}}' when: '"systemd" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_backtraces - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable core dump backtraces - Add missing configuration to correct section community.general.ini_file: path: '{{item}}' section: Coredump option: ProcessSizeMax value: '0' state: present no_extra_spaces: true when: - '"systemd" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int > 0 loop: '{{systemd_dropin_files_with_section.results | sum(attribute=''files'', start=[]) | map(attribute=''path'') | list }}' tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_backtraces - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable core dump backtraces - Add configuration to new remediation file community.general.ini_file: path: /etc/systemd/coredump.conf.d/complianceascode_hardening.conf section: Coredump option: ProcessSizeMax value: '0' state: present no_extra_spaces: true create: true when: - '"systemd" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int == 0 tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_backtraces - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,%23%20%20This%20file%20is%20part%20of%20systemd.%0A%23%0A%23%20%20systemd%20is%20free%20software%3B%20you%20can%20redistribute%20it%20and/or%20modify%20it%0A%23%20%20under%20the%20terms%20of%20the%20GNU%20Lesser%20General%20Public%20License%20as%20published%20by%0A%23%20%20the%20Free%20Software%20Foundation%3B%20either%20version%202.1%20of%20the%20License%2C%20or%0A%23%20%20%28at%20your%20option%29%20any%20later%20version.%0A%23%0A%23%20Entries%20in%20this%20file%20show%20the%20compile%20time%20defaults.%0A%23%20You%20can%20change%20settings%20by%20editing%20this%20file.%0A%23%20Defaults%20can%20be%20restored%20by%20simply%20deleting%20this%20file.%0A%23%0A%23%20See%20coredump.conf%285%29%20for%20details.%0A%0A%5BCoredump%5D%0A%23Storage%3Dexternal%0A%23Compress%3Dyes%0A%23ProcessSizeMax%3D2G%0A%23ExternalSizeMax%3D2G%0A%23JournalSizeMax%3D767M%0A%23MaxUse%3D%0A%23KeepFree%3D%0AStorage%3Dnone%0AProcessSizeMax%3D0%0A mode: 0644 path: /etc/systemd/coredump.conf overwrite: true Disable storing core dump The Storage option in [Coredump] section of /etc/systemd/coredump.conf or a drop-in file in /etc/systemd/coredump.conf.d/*.conf can be set to none to disable storing core dumps permanently. If the /etc/systemd/coredump.conf file or a drop-in file under /etc/systemd/coredump.conf.d/ does not already contain the [Coredump] section, the value will not be configured correctly. CM-6 Req-3.2 SRG-OS-000480-GPOS-00227 3.3.1.1 3.3.1 3.3 A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers or system operators trying to debug problems. Enabling core dumps on production systems is not recommended, however there may be overriding operational requirements to enable advanced debuging. Permitting temporary enablement of core dumps during such situations should be reviewed through local needs and policy. # Remediation is applicable only in certain platforms if rpm --quiet -q systemd; then found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/systemd/coredump.conf.d/complianceascode_hardening.conf /etc/systemd/coredump.conf.d/*.conf /etc/systemd/coredump.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[Coredump\]([^\n\[]*\n+)+?[[:space:]]*Storage" "$f"; then sed -i "s/Storage[^(\n)]*/Storage=none/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[Coredump\]" "$f"; then sed -i "/[[:space:]]*\[Coredump\]/a Storage=none" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/systemd/coredump.conf.d/complianceascode_hardening.conf /etc/systemd/coredump.conf.d/*.conf /etc/systemd/coredump.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[Coredump]\nStorage=none" >> "$file" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable storing core dump - Search for a section in files ansible.builtin.find: paths: '{{item.path}}' patterns: '{{item.pattern}}' contains: ^\s*\[Coredump\] read_whole_file: true use_regex: true register: systemd_dropin_files_with_section loop: - path: '{{ ''/etc/systemd/coredump.conf'' | dirname }}' pattern: '{{ ''/etc/systemd/coredump.conf'' | basename | regex_escape }}' - path: /etc/systemd/coredump.conf.d pattern: .*\.conf when: '"systemd" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable storing core dump - Count number of files which contain the correct section ansible.builtin.set_fact: count_of_systemd_dropin_files_with_section: '{{systemd_dropin_files_with_section.results | map(attribute=''matched'') | list | map(''int'') | sum}}' when: '"systemd" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable storing core dump - Add missing configuration to correct section community.general.ini_file: path: '{{item}}' section: Coredump option: Storage value: none state: present no_extra_spaces: true when: - '"systemd" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int > 0 loop: '{{systemd_dropin_files_with_section.results | sum(attribute=''files'', start=[]) | map(attribute=''path'') | list }}' tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable storing core dump - Add configuration to new remediation file community.general.ini_file: path: /etc/systemd/coredump.conf.d/complianceascode_hardening.conf section: Coredump option: Storage value: none state: present no_extra_spaces: true create: true when: - '"systemd" in ansible_facts.packages' - count_of_systemd_dropin_files_with_section | int == 0 tags: - NIST-800-53-CM-6 - PCI-DSS-Req-3.2 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - coredump_disable_storage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,%23%20%20This%20file%20is%20part%20of%20systemd.%0A%23%0A%23%20%20systemd%20is%20free%20software%3B%20you%20can%20redistribute%20it%20and/or%20modify%20it%0A%23%20%20under%20the%20terms%20of%20the%20GNU%20Lesser%20General%20Public%20License%20as%20published%20by%0A%23%20%20the%20Free%20Software%20Foundation%3B%20either%20version%202.1%20of%20the%20License%2C%20or%0A%23%20%20%28at%20your%20option%29%20any%20later%20version.%0A%23%0A%23%20Entries%20in%20this%20file%20show%20the%20compile%20time%20defaults.%0A%23%20You%20can%20change%20settings%20by%20editing%20this%20file.%0A%23%20Defaults%20can%20be%20restored%20by%20simply%20deleting%20this%20file.%0A%23%0A%23%20See%20coredump.conf%285%29%20for%20details.%0A%0A%5BCoredump%5D%0A%23Storage%3Dexternal%0A%23Compress%3Dyes%0A%23ProcessSizeMax%3D2G%0A%23ExternalSizeMax%3D2G%0A%23JournalSizeMax%3D767M%0A%23MaxUse%3D%0A%23KeepFree%3D%0AStorage%3Dnone%0AProcessSizeMax%3D0%0A mode: 0644 path: /etc/systemd/coredump.conf overwrite: true Disable Core Dumps for All Users To disable core dumps for all users, add the following line to /etc/security/limits.conf, or to a file within the /etc/security/limits.d/ directory: * hard core 0 1 12 13 15 16 2 7 8 APO13.01 BAI04.04 DSS01.03 DSS03.05 DSS05.07 SR 6.2 SR 7.1 SR 7.2 A.12.1.3 A.17.2.1 CM-6 SC-7(10) DE.CM-1 PR.DS-4 SRG-OS-000480-GPOS-00227 3.3.1.1 3.3.1 3.3 A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers trying to debug problems. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Set dirs, files and regex variables ansible.builtin.set_fact: limits_dropin_dir: /etc/security/limits.d limits_dropin_file: /etc/security/limits.d/10-ssg-hardening.conf limits_main_file: /etc/security/limits.conf limits_correct_regex: ^\s*\*\s+hard\s+core\s+0\s*$ when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Find valid drop-ins for core limit ansible.builtin.find: paths: '{{ limits_dropin_dir }}' patterns: '*.conf' contains: '{{ limits_correct_regex }}' file_type: file register: valid_dropins failed_when: false when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Find all drop-ins with any core limit ansible.builtin.find: paths: '{{ limits_dropin_dir }}' patterns: '*.conf' contains: ^\s*\*\s+hard\s+core\s+ file_type: file register: all_dropins failed_when: false when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Get invalid drop-ins ansible.builtin.set_fact: invalid_dropins: '{{ all_dropins.files | rejectattr(''path'', ''in'', valid_dropins.files | map(attribute=''path'') | list) | map(attribute=''path'') | list }}' when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Comment invalid * hard core lines in drop-ins ansible.builtin.replace: path: '{{ item }}' regexp: (^\s*\*\s+hard\s+core\s+.*$) replace: '#\1' loop: '{{ invalid_dropins }}' when: - '"pam" in ansible_facts.packages' - invalid_dropins | length > 0 tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Check if main limits.conf contains correct core limit ansible.builtin.find: paths: /etc/security patterns: limits.conf contains: '{{ limits_correct_regex }}' file_type: file register: main_valid failed_when: false when: - '"pam" in ansible_facts.packages' - not (valid_dropins.matched | default(0) > 0) tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Set fact if configuration is valid ansible.builtin.set_fact: core_limit_valid: '{{ (valid_dropins.matched | default(0)) > 0 or (main_valid.matched | default(0)) > 0 }}' when: '"pam" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Ensure drop-in directory exists ansible.builtin.file: path: '{{ limits_dropin_dir }}' state: directory when: - '"pam" in ansible_facts.packages' - not core_limit_valid tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Core Dumps for All Users - Deploy 10-ssg-hardening.conf drop-in with correct core limit ansible.builtin.copy: dest: '{{ limits_dropin_file }}' content: | * hard core 0 when: - '"pam" in ansible_facts.packages' - not core_limit_valid tags: - NIST-800-53-CM-6 - NIST-800-53-SC-7(10) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_users_coredumps - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,%2A%20%20%20%20%20hard%20%20%20core%20%20%20%200 mode: 0644 path: /etc/security/limits.d/75-disable_users_coredumps.conf overwrite: true Disable Core Dumps for SUID programs To set the runtime status of the fs.suid_dumpable kernel parameter, run the following command: $ sudo sysctl -w fs.suid_dumpable=0 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: fs.suid_dumpable = 0 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) SI-11(a) SI-11(b) R14 3.3.1.1 3.3.1 3.3 The core dump of a setuid program is more likely to contain sensitive data, as the program itself runs with greater privileges than the user who initiated execution of the program. Disabling the ability for any setuid program to write a core file decreases the risk of unauthorized access of such data. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of fs.suid_dumpable from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*fs.suid_dumpable.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "fs.suid_dumpable" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for fs.suid_dumpable # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w fs.suid_dumpable="0" fi # # If fs.suid_dumpable present in /etc/sysctl.conf, change value to "0" # else, add "fs.suid_dumpable = 0" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^fs.suid_dumpable") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^fs.suid_dumpable\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^fs.suid_dumpable\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_suid_dumpable - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*fs.suid_dumpable.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_suid_dumpable - name: Comment out any occurrences of fs.suid_dumpable from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*fs.suid_dumpable replace: '#fs.suid_dumpable' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_suid_dumpable - name: Ensure sysctl fs.suid_dumpable is set to 0 ansible.posix.sysctl: name: fs.suid_dumpable value: '0' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-SI-11(a) - NIST-800-53-SI-11(b) - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_fs_suid_dumpable Daemon Umask The umask is a per-process setting which limits the default permissions for creation of new files and directories. The system includes initialization scripts which set the default umask for system daemons. daemon umask Enter umask for daemons 022 027 022 Set Daemon Umask The file /etc/init.d/functions includes initialization parameters for most or all daemons started at boot time. Many daemons on the system already individually restrict themselves to a umask of 077 in their own init scripts. By default, the umask of 022 is set which prevents creation of group- or world-writable files. To set the umask for daemons expected by the profile, edit the following line: umask Setting the umask to too restrictive a setting can cause serious errors at runtime. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 The umask influences the permissions assigned to files created by a process at run time. An unnecessarily permissive umask could result in files being created with insecure permissions. Enable ExecShield ExecShield describes kernel features that provide protection against exploitation of memory corruption errors such as buffer overflows. These features include random placement of the stack and other memory regions, prevention of execution in memory that should only hold data, and special handling of text buffers. These protections are enabled by default on 32-bit systems and controlled through sysctl variables kernel.exec-shield and kernel.randomize_va_space. On the latest 64-bit systems, kernel.exec-shield cannot be enabled or disabled with sysctl. kernel.kptr_restrict Configure exposition of kernel pointer addresses 1 1 2 Enable ExecShield via sysctl By default on Fedora 64-bit systems, ExecShield is enabled and can only be disabled if the hardware does not support ExecShield or is disabled in /etc/default/grub. 12 15 8 APO13.01 DSS05.02 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.13.1.1 A.13.2.1 A.14.1.3 SC-39 CM-6(a) PR.PT-4 SRG-OS-000433-GPOS-00192 ExecShield uses the segmentation feature on all x86 systems to prevent execution in memory higher than a certain address. It writes an address as a limit in the code segment descriptor, to control where code can be executed, on a per-process basis. When the kernel places a process's memory regions such as the stack and heap higher than this address, the hardware prevents execution in that address range. This is enabled by default on the latest Red Hat and Fedora systems if supported by the hardware. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then grubby --update-kernel=ALL --remove-args=noexec else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-SC-39 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - sysctl_kernel_exec_shield - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --remove-args="noexec" when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-SC-39 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - sysctl_kernel_exec_shield Restrict Exposed Kernel Pointer Addresses Access To set the runtime status of the kernel.kptr_restrict kernel parameter, run the following command: $ sudo sysctl -w kernel.kptr_restrict= To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.kptr_restrict = CIP-002-5 R1.1 CIP-002-5 R1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 4.1 CIP-004-6 4.2 CIP-004-6 R2.2.3 CIP-004-6 R2.2.4 CIP-004-6 R2.3 CIP-004-6 R4 CIP-005-6 R1 CIP-005-6 R1.1 CIP-005-6 R1.2 CIP-007-3 R3 CIP-007-3 R3.1 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 CIP-007-3 R8.4 CIP-009-6 R.1.1 CIP-009-6 R4 SC-30 SC-30(2) SC-30(5) CM-6(a) FMT_SMF_EXT.1 SRG-OS-000132-GPOS-00067 SRG-OS-000433-GPOS-00192 SRG-OS-000480-GPOS-00227 R9 Exposing kernel pointers (through procfs or seq_printf()) exposes kernel writeable structures which may contain functions pointers. If a write vulnerability occurs in the kernel, allowing write access to any of this structure, the kernel can be compromised. This option disallow any program without the CAP_SYSLOG capability to get the addresses of kernel pointers by replacing them with 0. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.kptr_restrict from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.kptr_restrict.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.kptr_restrict" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" sysctl_kernel_kptr_restrict_value='' # # Set runtime for kernel.kptr_restrict # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.kptr_restrict="$sysctl_kernel_kptr_restrict_value" fi # # If kernel.kptr_restrict present in /etc/sysctl.conf, change value to appropriate value # else, add "kernel.kptr_restrict = value" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.kptr_restrict") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_kernel_kptr_restrict_value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.kptr_restrict\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.kptr_restrict\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - NIST-800-53-SC-30(5) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kptr_restrict - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.kptr_restrict.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - NIST-800-53-SC-30(5) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kptr_restrict - name: Comment out any occurrences of kernel.kptr_restrict from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.kptr_restrict replace: '#kernel.kptr_restrict' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - NIST-800-53-SC-30(5) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kptr_restrict - name: XCCDF Value sysctl_kernel_kptr_restrict_value # promote to variable set_fact: sysctl_kernel_kptr_restrict_value: !!str tags: - always - name: Ensure sysctl kernel.kptr_restrict is set ansible.posix.sysctl: name: kernel.kptr_restrict value: '{{ sysctl_kernel_kptr_restrict_value }}' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - NIST-800-53-SC-30(5) - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_kptr_restrict --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.kptr_restrict%3D1%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_kptr_restrict.conf overwrite: true Enable Randomized Layout of Virtual Address Space To set the runtime status of the kernel.randomize_va_space kernel parameter, run the following command: $ sudo sysctl -w kernel.randomize_va_space=2 To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d: kernel.randomize_va_space = 2 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) CIP-002-5 R1.1 CIP-002-5 R1.2 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 4.1 CIP-004-6 4.2 CIP-004-6 R2.2.3 CIP-004-6 R2.2.4 CIP-004-6 R2.3 CIP-004-6 R4 CIP-005-6 R1 CIP-005-6 R1.1 CIP-005-6 R1.2 CIP-007-3 R3 CIP-007-3 R3.1 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 CIP-007-3 R8.4 CIP-009-6 R.1.1 CIP-009-6 R4 SC-30 SC-30(2) CM-6(a) Req-2.2.1 SRG-OS-000433-GPOS-00193 SRG-OS-000480-GPOS-00227 SRG-APP-000450-CTR-001105 R9 3.3.1.1 3.3.1 3.3 Address space layout randomization (ASLR) makes it more difficult for an attacker to predict the location of attack code they have introduced into a process's address space during an attempt at exploitation. Additionally, ASLR makes it more difficult for an attacker to know the location of existing code in order to re-purpose it using return oriented programming (ROP) techniques. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # Comment out any occurrences of kernel.randomize_va_space from /etc/sysctl.d/*.conf files for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf; do # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf) if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi matching_list=$(grep -P '^(?!#).*[\s]*kernel.randomize_va_space.*$' $f | uniq ) if ! test -z "$matching_list"; then while IFS= read -r entry; do escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry") # comment out "kernel.randomize_va_space" matches to preserve user data sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f done <<< "$matching_list" fi done # # Set sysctl config file which to save the desired value # SYSCONFIG_FILE="/etc/sysctl.conf" # # Set runtime for kernel.randomize_va_space # if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then /sbin/sysctl -q -n -w kernel.randomize_va_space="2" fi # # If kernel.randomize_va_space present in /etc/sysctl.conf, change value to "2" # else, add "kernel.randomize_va_space = 2" to /etc/sysctl.conf # # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.randomize_va_space") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "2" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^kernel.randomize_va_space\\>" "${SYSCONFIG_FILE}"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^kernel.randomize_va_space\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}" else if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}" fi printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - PCI-DSS-Req-2.2.1 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_randomize_va_space - name: List /etc/sysctl.d/*.conf files ansible.builtin.find: paths: - /etc/sysctl.d/ - /run/sysctl.d/ - /usr/local/lib/sysctl.d/ - /usr/lib/sysctl.d/ contains: ^[\s]*kernel.randomize_va_space.*$ patterns: '*.conf' file_type: any register: find_sysctl_d when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - PCI-DSS-Req-2.2.1 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_randomize_va_space - name: Comment out any occurrences of kernel.randomize_va_space from config files ansible.builtin.replace: path: '{{ item.path }}' regexp: ^[\s]*kernel.randomize_va_space replace: '#kernel.randomize_va_space' loop: '{{ find_sysctl_d.files }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - PCI-DSS-Req-2.2.1 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_randomize_va_space - name: Ensure sysctl kernel.randomize_va_space is set to 2 ansible.posix.sysctl: name: kernel.randomize_va_space value: '2' sysctl_file: /etc/sysctl.conf state: present reload: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-CM-6(a) - NIST-800-53-SC-30 - NIST-800-53-SC-30(2) - PCI-DSS-Req-2.2.1 - PCI-DSSv4-3.3 - PCI-DSSv4-3.3.1 - PCI-DSSv4-3.3.1.1 - disable_strategy - low_complexity - medium_disruption - medium_severity - reboot_required - sysctl_kernel_randomize_va_space --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,kernel.randomize_va_space%3D2%0A mode: 0644 path: /etc/sysctl.d/75-sysctl_kernel_randomize_va_space.conf overwrite: true Enable Execute Disable (XD) or No Execute (NX) Support on x86 Systems Recent processors in the x86 family support the ability to prevent code execution on a per memory page basis. Generically and on AMD processors, this ability is called No Execute (NX), while on Intel processors it is called Execute Disable (XD). This ability can help prevent exploitation of buffer overflow vulnerabilities and should be activated whenever possible. Extra steps must be taken to ensure that this protection is enabled, particularly on 32-bit x86 systems. Other processors, such as Itanium and POWER, have included such support since inception and the standard kernel for those platforms supports the feature. This is enabled by default on the latest Oracle Linux, Red Hat and Fedora systems if supported by the hardware. Enable NX or XD Support in the BIOS Reboot the system and enter the BIOS or Setup configuration menu. Navigate the BIOS configuration menu and make sure that the option is enabled. The setting may be located under a Security section. Look for Execute Disable (XD) on Intel-based systems and No Execute (NX) on AMD-based systems. 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.7 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 SC-39 CM-6(a) PR.IP-1 SRG-OS-000433-GPOS-00192 SRG-APP-000450-CTR-001105 2.2.1 2.2 Computers with the ability to prevent this type of code execution frequently put an option in the BIOS that will allow users to turn the feature on or off at will. Install PAE Kernel on Supported 32-bit x86 Systems Systems that are using the 64-bit x86 kernel package do not need to install the kernel-PAE package because the 64-bit x86 kernel already includes this support. However, if the system is 32-bit and also supports the PAE and NX features as determined in the previous section, the kernel-PAE package should be installed to enable XD or NX support. The kernel-PAE package can be installed with the following command: $ sudo dnf install kernel-PAE The installation process should also have configured the bootloader to load the new kernel at boot. Verify this after reboot and modify /etc/default/grub if necessary. The kernel-PAE package should not be installed on older systems that do not support the XD or NX bit, as 8this may prevent them from booting.8 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.7 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-6(a) PR.IP-1 R1 2.2.1 2.2 On 32-bit systems that support the XD or NX bit, the vendor-supplied PAE kernel is required to enable either Execute Disable (XD) or No Execute (NX) support. Memory Poisoning Memory Poisoning consists of writing a special value to uninitialized or freed memory. Poisoning can be used as a mechanism to prevent leak of information and detection of corrupted memory. slub_debug - debug options Defines the debug options to use in slub_debug kernel command line argument. P F Z P FZ FZP Enable page allocator poisoning To enable poisoning of free pages, add the argument page_poison=1 to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain page_poison=1 as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) page_poison=1" CM-6(a) SRG-OS-000480-GPOS-00227 SRG-OS-000134-GPOS-00068 R8 Poisoning writes an arbitrary value to freed pages, so any modification or reference to that page after being freed or before being initialized will be detected and prevented. This prevents many types of use-after-free vulnerabilities at little performance cost. Also prevents leak of data and detection of corrupted memory. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q grub2-common; }; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "page_poison" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"page_poison=[^\"]*\"(.*]\s*)/\1\"page_poison=1\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"page_poison=1\"]" >> "$KARGS_DIR/10-page_poison.toml" fi else grubby --update-kernel=ALL --args=page_poison=1 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - grub2_page_poison_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="page_poison=1" when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - grub2_page_poison_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy [customizations.kernel] append = "page_poison=1" bootloader page_poison=1 Enable SLUB/SLAB allocator poisoning To enable poisoning of SLUB/SLAB objects, add the argument slub_debug= to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain slub_debug= as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) slub_debug=" CM-6(a) SRG-OS-000433-GPOS-00192 SRG-OS-000134-GPOS-00068 R8 Poisoning writes an arbitrary value to freed objects, so any modification or reference to that object after being freed or before being initialized will be detected and prevented. This prevents many types of use-after-free vulnerabilities at little performance cost. Also prevents leak of data and detection of corrupted memory. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q grub2-common; }; then var_slub_debug_options='' if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "slub_debug" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"slub_debug=[^\"]*\"(.*]\s*)/\1\"slub_debug=$var_slub_debug_options\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"slub_debug=$var_slub_debug_options\"]" >> "$KARGS_DIR/10-slub_debug.toml" fi else grubby --update-kernel=ALL --args=slub_debug=$var_slub_debug_options fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - grub2_slub_debug_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy - name: XCCDF Value var_slub_debug_options # promote to variable set_fact: var_slub_debug_options: !!str tags: - always - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="slub_debug={{ var_slub_debug_options }}" when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - grub2_slub_debug_argument - low_disruption - medium_complexity - medium_severity - reboot_required - restrict_strategy [customizations.kernel] append = "slub_debug=" bootloader slub_debug= SELinux SELinux is a feature of the Linux kernel which can be used to guard against misconfigured or compromised programs. SELinux enforces the idea that programs should be limited in what files they can access and what actions they can take. The default SELinux policy, as configured on Fedora, has been sufficiently developed and debugged that it should be usable on almost any system with minimal configuration and a small amount of system administrator training. This policy prevents system services - including most of the common network-visible services such as mail servers, FTP servers, and DNS servers - from accessing files which those services have no valid reason to access. This action alone prevents a huge amount of possible damage from network attacks against services, from trojaned software, and so forth. This guide recommends that SELinux be enabled using the default (targeted) policy on every Fedora system, unless that system has unusual requirements which make a stronger policy appropriate. SELinux policy Type of policy in use. Possible values are: targeted - Only targeted network daemons are protected. strict - Full SELinux protection. mls - Multiple levels of security targeted mls targeted SELinux state enforcing - SELinux security policy is enforced. permissive - SELinux prints warnings instead of enforcing. disabled - SELinux is fully disabled. enforcing disabled enforcing permissive Install libselinux Package The libselinux package can be installed with the following command: $ sudo dnf install libselinux 1.3.1.1 1.2.6 1.2 Security-enhanced Linux is a feature of the Linux kernel and a number of utilities with enhanced security functionality designed to add mandatory access controls to Linux. The libselinux package contains the core library of the Security-enhanced Linux system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "libselinux" ; then dnf install -y "libselinux" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - enable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_libselinux_installed - name: Ensure libselinux is installed ansible.builtin.package: name: libselinux state: present when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - enable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_libselinux_installed include install_libselinux class install_libselinux { package { 'libselinux': ensure => 'installed', } } package --add=libselinux [[packages]] name = "libselinux" version = "*" package install libselinux dnf install libselinux Install policycoreutils Package The policycoreutils package can be installed with the following command: $ sudo dnf install policycoreutils SRG-OS-000480-GPOS-00227 SRG-OS-000134-GPOS-00068 Security-enhanced Linux is a feature of the Linux kernel and a number of utilities with enhanced security functionality designed to add mandatory access controls to Linux. The Security-enhanced Linux kernel contains new architectural components originally developed to improve security of the Flask operating system. These architectural components provide general support for the enforcement of many kinds of mandatory access control policies, including those based on the concepts of Type Enforcement, Role-based Access Control, and Multi-level Security. policycoreutils contains the policy core utilities that are required for basic operation of an SELinux-enabled system. These utilities include load_policy to load SELinux policies, setfiles to label filesystems, newrole to switch roles, and so on. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "policycoreutils" ; then dnf install -y "policycoreutils" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_policycoreutils_installed - name: Ensure policycoreutils is installed ansible.builtin.package: name: policycoreutils state: present when: '"kernel" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_policycoreutils_installed include install_policycoreutils class install_policycoreutils { package { 'policycoreutils': ensure => 'installed', } } package --add=policycoreutils [[packages]] name = "policycoreutils" version = "*" package install policycoreutils dnf install policycoreutils Uninstall mcstrans Package The mcstransd daemon provides category label information to client processes requesting information. The label translations are defined in /etc/selinux/targeted/setrans.conf. The mcstrans package can be removed with the following command: $ sudo dnf remove mcstrans 1.3.1.7 Since this service is not used very often, disable it to reduce the amount of potentially vulnerable code running on the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # CAUTION: This remediation script will remove mcstrans # from the system, and may remove any packages # that depend on mcstrans. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "mcstrans" ; then dnf remove -y --noautoremove "mcstrans" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_mcstrans_removed - name: 'Uninstall mcstrans Package: Ensure mcstrans is removed' ansible.builtin.package: name: mcstrans state: absent when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_mcstrans_removed include remove_mcstrans class remove_mcstrans { package { 'mcstrans': ensure => 'purged', } } package --remove=mcstrans package remove mcstrans dnf remove mcstrans Uninstall setroubleshoot-plugins Package The SETroubleshoot plugins are used to analyze SELinux AVC data. The service provides information around configuration errors, unauthorized intrusions, and other potential errors. The setroubleshoot-plugins package can be removed with the following command: $ sudo dnf remove setroubleshoot-plugins R49 The SETroubleshoot service is an unnecessary daemon to have running on a server. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # CAUTION: This remediation script will remove setroubleshoot-plugins # from the system, and may remove any packages # that depend on setroubleshoot-plugins. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "setroubleshoot-plugins" ; then dnf remove -y --noautoremove "setroubleshoot-plugins" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_setroubleshoot-plugins_removed - name: 'Uninstall setroubleshoot-plugins Package: Ensure setroubleshoot-plugins is removed' ansible.builtin.package: name: setroubleshoot-plugins state: absent when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_setroubleshoot-plugins_removed include remove_setroubleshoot-plugins class remove_setroubleshoot-plugins { package { 'setroubleshoot-plugins': ensure => 'purged', } } package --remove=setroubleshoot-plugins package remove setroubleshoot-plugins dnf remove setroubleshoot-plugins Uninstall setroubleshoot-server Package The SETroubleshoot service notifies desktop users of SELinux denials. The service provides information around configuration errors, unauthorized intrusions, and other potential errors. The setroubleshoot-server package can be removed with the following command: $ sudo dnf remove setroubleshoot-server R49 The SETroubleshoot service is an unnecessary daemon to have running on a server. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # CAUTION: This remediation script will remove setroubleshoot-server # from the system, and may remove any packages # that depend on setroubleshoot-server. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "setroubleshoot-server" ; then dnf remove -y --noautoremove "setroubleshoot-server" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_setroubleshoot-server_removed - name: 'Uninstall setroubleshoot-server Package: Ensure setroubleshoot-server is removed' ansible.builtin.package: name: setroubleshoot-server state: absent when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_setroubleshoot-server_removed include remove_setroubleshoot-server class remove_setroubleshoot-server { package { 'setroubleshoot-server': ensure => 'purged', } } package --remove=setroubleshoot-server package remove setroubleshoot-server dnf remove setroubleshoot-server Uninstall setroubleshoot Package The SETroubleshoot service notifies desktop users of SELinux denials. The service provides information around configuration errors, unauthorized intrusions, and other potential errors. The setroubleshoot package can be removed with the following command: $ sudo dnf remove setroubleshoot R49 1.3.1.8 The SETroubleshoot service is an unnecessary daemon to have running on a server, especially if X Windows is removed or disabled. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # CAUTION: This remediation script will remove setroubleshoot # from the system, and may remove any packages # that depend on setroubleshoot. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "setroubleshoot" ; then dnf remove -y --noautoremove "setroubleshoot" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_setroubleshoot_removed - name: 'Uninstall setroubleshoot Package: Ensure setroubleshoot is removed' ansible.builtin.package: name: setroubleshoot state: absent when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_setroubleshoot_removed include remove_setroubleshoot class remove_setroubleshoot { package { 'setroubleshoot': ensure => 'purged', } } package --remove=setroubleshoot package remove setroubleshoot dnf remove setroubleshoot Ensure SELinux Not Disabled in the kernel arguments SELinux can be disabled at boot time by disabling it via a kernel argument. Remove any instances of selinux=0 from the kernel arguments in that file to prevent SELinux from being disabled at boot. 1 11 12 13 14 15 16 18 3 4 5 6 8 9 APO01.06 APO11.04 APO13.01 BAI03.05 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.03 DSS06.06 MEA02.01 3.1.2 3.7.2 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) 4.2.3.4 4.3.3.2.2 4.3.3.3.9 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-3 AC-3(3)(a) DE.AE-1 ID.AM-3 PR.AC-4 PR.AC-5 PR.AC-6 PR.DS-5 PR.PT-1 PR.PT-3 PR.PT-4 SRG-APP-000233-CTR-000585 APP.4.4.A4 SYS.1.6.A3 SYS.1.6.A18 SYS.1.6.A21 Disabling a major host protection feature, such as SELinux, at boot time prevents it from confining system services at boot time. Further, it increases the chances that it will remain off during system operation. Ensure SELinux Not Disabled in /etc/default/grub SELinux can be disabled at boot time by an argument in /etc/default/grub. Remove any instances of selinux=0 from the kernel arguments in that file to prevent SELinux from being disabled at boot. 1 11 12 13 14 15 16 18 3 4 5 6 8 9 APO01.06 APO11.04 APO13.01 BAI03.05 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.03 DSS06.06 MEA02.01 3.1.2 3.7.2 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) 4.2.3.4 4.3.3.2.2 4.3.3.3.9 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-3 AC-3(3)(a) DE.AE-1 ID.AM-3 PR.AC-4 PR.AC-5 PR.AC-6 PR.DS-5 PR.PT-1 PR.PT-3 PR.PT-4 1.3.1.2 1.2.6 1.2 Disabling a major host protection feature, such as SELinux, at boot time prevents it from confining system services at boot time. Further, it increases the chances that it will remain off during system operation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q grub2-common; }; then sed -i --follow-symlinks "s/selinux=0//gI" /etc/default/grub /etc/grub2.cfg /etc/grub.d/* sed -i --follow-symlinks "s/enforcing=0//gI" /etc/default/grub /etc/grub2.cfg /etc/grub.d/* else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - grub2_enable_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure SELinux Not Disabled in /etc/default/grub - Find /etc/grub.d/ files ansible.builtin.find: paths: - /etc/grub.d/ follow: true register: result_grub_d when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - grub2_enable_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure SELinux Not Disabled in /etc/default/grub - Ensure SELinux Not Disabled in /etc/grub.d/ files ansible.builtin.replace: dest: '{{ item.path }}' regexp: (selinux|enforcing)=0 with_items: - '{{ result_grub_d.files }}' when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - grub2_enable_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure SELinux Not Disabled in /etc/default/grub - Check if /etc/grub2.cfg exists ansible.builtin.stat: path: /etc/grub2.cfg register: result_grub2_cfg_present when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - grub2_enable_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure SELinux Not Disabled in /etc/default/grub - Check if /etc/default/grub exists ansible.builtin.stat: path: /etc/default/grub register: result_default_grub_present when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - grub2_enable_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure SELinux Not Disabled in /etc/default/grub - Ensure SELinux Not Disabled in /etc/grub2.cfg ansible.builtin.replace: dest: /etc/grub2.cfg regexp: (selinux|enforcing)=0 when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' - result_grub2_cfg_present.stat.exists tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - grub2_enable_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure SELinux Not Disabled in /etc/default/grub - Ensure SELinux Not Disabled in /etc/default/grub ansible.builtin.replace: dest: /etc/default/grub regexp: (selinux|enforcing)=0 when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' - result_default_grub_present.stat.exists tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - grub2_enable_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure No Device Files are Unlabeled by SELinux Device files, which are used for communication with important system resources, should be labeled with proper SELinux types. If any device files carry the SELinux type device_t or unlabeled_t, report the bug so that policy can be corrected. Supply information about what the device is and what programs use it. To check for incorrectly labeled device files, run following commands: $ sudo find /dev -context *:device_t:* \( -type c -o -type b \) -printf "%p %Z\n" $ sudo find /dev -context *:unlabeled_t:* \( -type c -o -type b \) -printf "%p %Z\n" It should produce no output in a well-configured system. Automatic remediation of this control is not available. The remediation can be achieved by amending SELinux policy. 1 11 12 13 14 15 16 18 2 3 5 6 7 8 9 APO01.06 APO11.04 BAI01.06 BAI03.05 BAI06.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 MEA02.01 3.1.2 3.1.5 3.7.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 2.8 SR 2.9 SR 5.2 SR 6.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.5.1 A.12.6.2 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.14.2.7 A.15.2.1 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) AC-3(3)(a) AC-6 DE.CM-1 DE.CM-7 PR.AC-4 PR.DS-5 PR.IP-1 PR.IP-3 PR.PT-1 PR.PT-3 SRG-OS-000480-GPOS-00227 If a device file carries the SELinux type device_t or unlabeled_t, then SELinux cannot properly restrict access to the device file. Ensure No Daemons are Unconfined by SELinux Daemons for which the SELinux policy does not contain rules will inherit the context of the parent process. Because daemons are launched during startup and descend from the init process, they inherit the unconfined_service_t context. To check for unconfined daemons, run the following command: $ sudo ps -eZ | grep "unconfined_service_t" It should produce no output in a well-configured system. Automatic remediation of this control is not available. Remediation can be achieved by amending SELinux policy or stopping the unconfined daemons as outlined above. 1 11 12 13 14 15 16 18 3 5 6 9 APO01.06 APO11.04 BAI03.05 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.06 MEA02.01 3.1.2 3.1.5 3.7.2 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 2.8 SR 2.9 SR 5.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.5.1 A.12.6.2 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-7(a) CM-7(b) CM-6(a) AC-3(3)(a) AC-6 PR.AC-4 PR.DS-5 PR.IP-1 PR.PT-1 PR.PT-3 1.2.6 1.2 Daemons which run with the unconfined_service_t context may cause AVC denials, or allow privileges that the daemon does not require. Ensure SELinux is Not Disabled The SELinux state should be set to enforcing or permissive at system boot time. In the file /etc/selinux/config, add or correct the following line to configure the system to boot into enforcing or permissive mode: SELINUX=enforcing OR SELINUX=permissive Ensure that all files have correct SELinux labels by running: fixfiles onboot Then reboot the system. In case the SELinux is "disabled", the automated remediation will adopt a more conservative approach and set it to "permissive" in order to avoid any system disruption and give the administrator the opportunity to assess the impact and necessary efforts before setting it to "enforcing", which is strongly recommended. 1.3.1.4 Running SELinux in disabled mode is strongly discouraged. It prevents enforcing the SELinux controls without a system reboot. It also avoids labeling any persistent objects such as files, making it difficult to enable SELinux in the future. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if [ -e "/etc/selinux/config" ] ; then LC_ALL=C sed -i "/^SELINUX=/Id" "/etc/selinux/config" else touch "/etc/selinux/config" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/selinux/config" cp "/etc/selinux/config" "/etc/selinux/config.bak" # Insert at the end of the file printf '%s\n' "SELINUX=permissive" >> "/etc/selinux/config" # Clean up after ourselves. rm "/etc/selinux/config.bak" fixfiles onboot else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - high_severity - low_complexity - low_disruption - reboot_required - restrict_strategy - selinux_not_disabled - name: Ensure SELinux is Not Disabled block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUX= state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/selinux/config ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUX= state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/selinux/config ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUX= line: SELINUX=permissive state: present when: '"kernel" in ansible_facts.packages' tags: - high_severity - low_complexity - low_disruption - reboot_required - restrict_strategy - selinux_not_disabled - name: ' - Mark system to relabel SELinux on next boot' ansible.builtin.file: path: /.autorelabel state: touch when: '"kernel" in ansible_facts.packages' tags: - high_severity - low_complexity - low_disruption - reboot_required - restrict_strategy - selinux_not_disabled Configure SELinux Policy The SELinux targeted policy is appropriate for general-purpose desktops and servers, as well as systems in many other roles. To configure the system to use this policy, add or correct the following line in /etc/selinux/config: SELINUXTYPE= Other policies, such as mls, provide additional security labeling and greater confinement but are not compatible with many general-purpose use cases. 1 11 12 13 14 15 16 18 3 4 5 6 8 9 APO01.06 APO11.04 APO13.01 BAI03.05 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.03 DSS06.06 MEA02.01 3.1.2 3.7.2 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) 4.2.3.4 4.3.3.2.2 4.3.3.3.9 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.2 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-004-6 R3.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 CIP-007-3 R6.5 AC-3 AC-3(3)(a) AU-9 SC-7(21) DE.AE-1 ID.AM-3 PR.AC-4 PR.AC-5 PR.AC-6 PR.DS-5 PR.PT-1 PR.PT-3 PR.PT-4 FMT_MOF_EXT.1 SRG-OS-000445-GPOS-00199 SRG-APP-000233-CTR-000585 R46 R64 APP.4.4.A4 SYS.1.6.A3 SYS.1.6.A18 SYS.1.6.A21 1.3.1.3 1.2.6 1.2 Setting the SELinux policy to targeted or a more specialized policy ensures the system will confine processes that are likely to be targeted for exploitation, such as network or system services. Note: During the development or debugging of SELinux modules, it is common to temporarily place non-production systems in permissive mode. In such temporary cases, SELinux policies should be developed, and once work is completed, the system should be reconfigured to . # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_selinux_policy_name='' if [ -e "/etc/selinux/config" ] ; then LC_ALL=C sed -i "/^SELINUXTYPE=/Id" "/etc/selinux/config" else touch "/etc/selinux/config" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/selinux/config" cp "/etc/selinux/config" "/etc/selinux/config.bak" # Insert at the end of the file printf '%s\n' "SELINUXTYPE=$var_selinux_policy_name" >> "/etc/selinux/config" # Clean up after ourselves. rm "/etc/selinux/config.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - NIST-800-53-AU-9 - NIST-800-53-SC-7(21) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - selinux_policytype - name: XCCDF Value var_selinux_policy_name # promote to variable set_fact: var_selinux_policy_name: !!str tags: - always - name: Configure SELinux Policy block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUXTYPE= state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/selinux/config ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUXTYPE= state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/selinux/config ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUXTYPE= line: SELINUXTYPE={{ var_selinux_policy_name }} state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - NIST-800-53-AU-9 - NIST-800-53-SC-7(21) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - selinux_policytype Ensure SELinux State is Enforcing The SELinux state should be set to at system boot time. In the file /etc/selinux/config, add or correct the following line to configure the system to boot into enforcing mode: SELINUX= Ensure that all files have correct SELinux labels by running: fixfiles onboot Then reboot the system. 1 11 12 13 14 15 16 18 3 4 5 6 8 9 APO01.06 APO11.04 APO13.01 BAI03.05 DSS01.05 DSS03.01 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.03 DSS06.06 MEA02.01 3.1.2 3.7.2 164.308(a)(1)(ii)(D) 164.308(a)(3) 164.308(a)(4) 164.310(b) 164.310(c) 164.312(a) 164.312(e) 4.2.3.4 4.3.3.2.2 4.3.3.3.9 4.3.3.4 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 4.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.1 A.12.1.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.2 A.13.1.3 A.13.2.1 A.13.2.2 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.2 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-004-6 R3.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 CIP-007-3 R6.5 AC-3 AC-3(3)(a) AU-9 SC-7(21) DE.AE-1 ID.AM-3 PR.AC-4 PR.AC-5 PR.AC-6 PR.DS-5 PR.PT-1 PR.PT-3 PR.PT-4 FMT_MOF_EXT.1 SRG-OS-000445-GPOS-00199 SRG-OS-000134-GPOS-00068 R37 R79 APP.4.4.A4 SYS.1.6.A3 SYS.1.6.A18 SYS.1.6.A21 1.3.1.5 1.2.6 1.2 Setting the SELinux state to enforcing ensures SELinux is able to confine potentially compromised processes to the security policy, which is designed to prevent them from causing damage to the system or further elevating their privileges. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_selinux_state='' if [ -e "/etc/selinux/config" ] ; then LC_ALL=C sed -i "/^SELINUX=/Id" "/etc/selinux/config" else touch "/etc/selinux/config" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/selinux/config" cp "/etc/selinux/config" "/etc/selinux/config.bak" # Insert at the end of the file printf '%s\n' "SELINUX=$var_selinux_state" >> "/etc/selinux/config" # Clean up after ourselves. rm "/etc/selinux/config.bak" fixfiles onboot else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - NIST-800-53-AU-9 - NIST-800-53-SC-7(21) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - selinux_state - name: XCCDF Value var_selinux_state # promote to variable set_fact: var_selinux_state: !!str tags: - always - name: Ensure SELinux State is Enforcing block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUX= state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/selinux/config ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUX= state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/selinux/config ansible.builtin.lineinfile: path: /etc/selinux/config create: true regexp: (?i)^SELINUX= line: SELINUX={{ var_selinux_state }} state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - NIST-800-53-AU-9 - NIST-800-53-SC-7(21) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - selinux_state - name: ' - Mark system to relabel SELinux on next boot' ansible.builtin.file: path: /.autorelabel state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.2 - NIST-800-171-3.7.2 - NIST-800-53-AC-3 - NIST-800-53-AC-3(3)(a) - NIST-800-53-AU-9 - NIST-800-53-SC-7(21) - PCI-DSSv4-1.2 - PCI-DSSv4-1.2.6 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - selinux_state Services The best protection against vulnerable software is running less software. This section describes how to review the software which Fedora installs on a system and disable software which is not needed. It then enumerates the software packages installed on a default Fedora system and provides guidance about which ones can be safely disabled. Fedora provides a convenient minimal install option that essentially installs the bare necessities for a functional system. When building Fedora systems, it is highly recommended to select the minimal packages and then build up the system from there. Avahi Server The Avahi daemon implements the DNS Service Discovery and Multicast DNS protocols, which provide service and host discovery on a network. It allows a system to automatically identify resources on the network, such as printers or web servers. This capability is also known as mDNSresponder and is a major part of Zeroconf networking. Configure Avahi if Necessary If your system requires the Avahi daemon, its configuration can be restricted to improve security. The Avahi daemon configuration file is /etc/avahi/avahi-daemon.conf. The following security recommendations should be applied to this file: See the avahi-daemon.conf(5) man page, or documentation at http://www.avahi.org, for more detailed information about the configuration options. Disable Avahi Publishing To prevent Avahi from publishing its records, edit /etc/avahi/avahi-daemon.conf and ensure the following line appears in the [publish] section: disable-publishing=yes 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 This helps ensure that no record will be published by Avahi. Disable Avahi Server if Possible Because the Avahi daemon service keeps an open network port, it is subject to network attacks. Disabling it can reduce the system's vulnerability to such attacks. Disable Avahi Server Software The avahi-daemon service can be disabled with the following command: $ sudo systemctl mask --now avahi-daemon.service 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 2.1.2 2.2.4 2.2 Because the Avahi daemon service keeps an open network port, it is subject to network attacks. Its functionality is convenient but is only appropriate if the local network can be trusted. # Remediation is applicable only in certain platforms if ( rpm --quiet -q avahi && rpm --quiet -q kernel ); then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'avahi-daemon.service' fi "$SYSTEMCTL_EXEC" disable 'avahi-daemon.service' "$SYSTEMCTL_EXEC" mask 'avahi-daemon.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files avahi-daemon.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'avahi-daemon.socket' fi "$SYSTEMCTL_EXEC" mask 'avahi-daemon.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'avahi-daemon.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_avahi-daemon_disabled - name: Disable Avahi Server Software - Disable service avahi-daemon block: - name: Disable Avahi Server Software - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable Avahi Server Software - Ensure avahi-daemon.service is Masked ansible.builtin.systemd: name: avahi-daemon.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("avahi-daemon.service", multiline=True) - name: Unit Socket Exists - avahi-daemon.socket ansible.builtin.command: systemctl -q list-unit-files avahi-daemon.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable Avahi Server Software - Disable Socket avahi-daemon ansible.builtin.systemd: name: avahi-daemon.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("avahi-daemon.socket", multiline=True) tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_avahi-daemon_disabled - special_service_block when: ( "avahi" in ansible_facts.packages and "kernel" in ansible_facts.packages ) include disable_avahi-daemon class disable_avahi-daemon { service {'avahi-daemon': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: avahi-daemon.service enabled: false mask: true - name: avahi-daemon.socket enabled: false mask: true [customizations.services] masked = ["avahi-daemon"] service disable avahi-daemon Base Services This section addresses the base services that are installed on a Fedora default installation which are not covered in other sections. Some of these services listen on the network and should be treated with particular discretion. Other services are local system utilities that may or may not be extraneous. In general, system services should be disabled if not required. Uninstall Automatic Bug Reporting Tool (abrt) The Automatic Bug Reporting Tool (abrt) collects and reports crash data when an application crash is detected. Using a variety of plugins, abrt can email crash reports to system administrators, log crash reports to files, or forward crash reports to a centralized issue tracking system such as RHTSupport. The abrt package can be removed with the following command: $ sudo dnf remove abrt SRG-OS-000095-GPOS-00049 Mishandling crash data could expose sensitive information about vulnerabilities in software executing on the system, as well as sensitive information from within a process's address space or registers. # CAUTION: This remediation script will remove abrt # from the system, and may remove any packages # that depend on abrt. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "abrt" ; then dnf remove -y --noautoremove "abrt" fi - name: 'Uninstall Automatic Bug Reporting Tool (abrt): Ensure abrt is removed' ansible.builtin.package: name: abrt state: absent tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_abrt_removed include remove_abrt class remove_abrt { package { 'abrt': ensure => 'purged', } } package --remove=abrt package remove abrt dnf remove abrt Cron and At Daemons The cron and at services are used to allow commands to be executed at a later time. The cron service is required by almost all systems to perform necessary maintenance tasks, while at may or may not be required on a given system. Both daemons should be configured defensively. Install the cron service The Cron service should be installed. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-6(a) PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 2.4.1.1 2.2.6 2.2 The cron service allow periodic job execution, needed for almost all administrative tasks and services (software update, log rotating, etc.). Access to cron service should be restricted to administrative accounts only. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "cron" ; then dnf install -y "cron" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_cron_installed - name: Ensure cron is installed ansible.builtin.package: name: cron state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_cron_installed include install_cron class install_cron { package { 'cron': ensure => 'installed', } } package --add=cron [[packages]] name = "cron" version = "*" package install cron dnf install cron Enable cron Service The crond service is used to execute commands at preconfigured times. It is required by almost all systems to perform necessary maintenance tasks, such as notifying root of system activity. The cron service can be enabled with the following command: $ sudo systemctl enable cron.service 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-6(a) PR.IP-1 PR.PT-3 Due to its usage for maintenance and security-supporting tasks, enabling the cron daemon is essential. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'cron.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'cron.service' fi "$SYSTEMCTL_EXEC" enable 'cron.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_cron_enabled - name: Enable cron Service - Enable service cron block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable cron Service - Enable Service cron ansible.builtin.systemd: name: cron enabled: true state: started masked: false when: - '"cron" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_cron_enabled - special_service_block when: '"kernel" in ansible_facts.packages' include enable_cron class enable_cron { service {'cron': enable => true, ensure => 'running', } } [customizations.services] enabled = ["cron"] service enable cron Enable cron Service The crond service is used to execute commands at preconfigured times. It is required by almost all systems to perform necessary maintenance tasks, such as notifying root of system activity. The crond service can be enabled with the following command: $ sudo systemctl enable crond.service 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-6(a) PR.IP-1 PR.PT-3 2.4.1.1 Due to its usage for maintenance and security-supporting tasks, enabling the cron daemon is essential. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'crond.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'crond.service' fi "$SYSTEMCTL_EXEC" enable 'crond.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_crond_enabled - name: Enable cron Service - Enable service crond block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable cron Service - Enable Service crond ansible.builtin.systemd: name: crond enabled: true state: started masked: false when: - '"cronie" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_crond_enabled - special_service_block when: '"kernel" in ansible_facts.packages' include enable_crond class enable_crond { service {'crond': enable => true, ensure => 'running', } } [customizations.services] enabled = ["crond"] service enable crond Disable At Service (atd) The at and batch commands can be used to schedule tasks that are meant to be executed only once. This allows delayed execution in a manner similar to cron, except that it is not recurring. The daemon atd keeps track of tasks scheduled via at and batch, and executes them at the specified time. The atd service can be disabled with the following command: $ sudo systemctl mask --now atd.service 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 The atd service could be used by an unsophisticated insider to carry out activities outside of a normal login session, which could complicate accountability. Furthermore, the need to schedule tasks with at or batch is not common. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'atd.service' fi "$SYSTEMCTL_EXEC" disable 'atd.service' "$SYSTEMCTL_EXEC" mask 'atd.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files atd.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'atd.socket' fi "$SYSTEMCTL_EXEC" mask 'atd.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'atd.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_atd_disabled - name: Disable At Service (atd) - Disable service atd block: - name: Disable At Service (atd) - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable At Service (atd) - Ensure atd.service is Masked ansible.builtin.systemd: name: atd.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("atd.service", multiline=True) - name: Unit Socket Exists - atd.socket ansible.builtin.command: systemctl -q list-unit-files atd.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable At Service (atd) - Disable Socket atd ansible.builtin.systemd: name: atd.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("atd.socket", multiline=True) tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_atd_disabled - special_service_block when: '"kernel" in ansible_facts.packages' include disable_atd class disable_atd { service {'atd': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: atd.service enabled: false mask: true - name: atd.socket enabled: false mask: true [customizations.services] masked = ["atd"] service disable atd Disable anacron Service The cronie-anacron package, which provides anacron functionality, is installed by default. The cronie-anacron package can be removed with the following command: $ sudo dnf remove cronie-anacron 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 The anacron service provides cron functionality for systems such as laptops and workstations that may be shut down during the normal times that cron jobs are scheduled to run. On systems which do not require this additional functionality, anacron could needlessly increase the possible attack surface for an intruder. Verify Group Who Owns cron.d To properly set the group owner of /etc/cron.d, run the command: $ sudo chgrp root /etc/cron.d 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.8 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /etc/cron.d/ -maxdepth 0 -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_cron_d_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_cron_d_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/cron.d/ ansible.builtin.file: path: /etc/cron.d/ follow: false state: directory group: '{{ file_groupowner_cron_d_newgroup }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns cron.daily To properly set the group owner of /etc/cron.daily, run the command: $ sudo chgrp root /etc/cron.daily 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.4 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /etc/cron.daily/ -maxdepth 0 -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_cron_daily_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_cron_daily_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/cron.daily/ ansible.builtin.file: path: /etc/cron.daily/ follow: false state: directory group: '{{ file_groupowner_cron_daily_newgroup }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns cron.hourly To properly set the group owner of /etc/cron.hourly, run the command: $ sudo chgrp root /etc/cron.hourly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.3 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /etc/cron.hourly/ -maxdepth 0 -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_cron_hourly_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_cron_hourly_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/cron.hourly/ ansible.builtin.file: path: /etc/cron.hourly/ follow: false state: directory group: '{{ file_groupowner_cron_hourly_newgroup }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns cron.monthly To properly set the group owner of /etc/cron.monthly, run the command: $ sudo chgrp root /etc/cron.monthly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.6 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /etc/cron.monthly/ -maxdepth 0 -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_cron_monthly_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_cron_monthly_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/cron.monthly/ ansible.builtin.file: path: /etc/cron.monthly/ follow: false state: directory group: '{{ file_groupowner_cron_monthly_newgroup }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns cron.weekly To properly set the group owner of /etc/cron.weekly, run the command: $ sudo chgrp root /etc/cron.weekly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.5 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /etc/cron.weekly/ -maxdepth 0 -type d ! -group 0 -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_cron_weekly_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_cron_weekly_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/cron.weekly/ ansible.builtin.file: path: /etc/cron.weekly/ follow: false state: directory group: '{{ file_groupowner_cron_weekly_newgroup }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns Crontab To properly set the group owner of /etc/crontab, run the command: $ sudo chgrp root /etc/crontab 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.2 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/crontab" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/crontab fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_crontab_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_crontab_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/crontab ansible.builtin.stat: path: /etc/crontab register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/crontab ansible.builtin.file: path: /etc/crontab follow: false group: '{{ file_groupowner_crontab_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Owner on cron.d To properly set the owner of /etc/cron.d, run the command: $ sudo chown root /etc/cron.d 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.8 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct user to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/cron.d/ -maxdepth 0 -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_cron_d_newown variable if represented by uid ansible.builtin.set_fact: file_owner_cron_d_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /etc/cron.d/ ansible.builtin.file: path: /etc/cron.d/ follow: false state: directory owner: '{{ file_owner_cron_d_newown }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Owner on cron.daily To properly set the owner of /etc/cron.daily, run the command: $ sudo chown root /etc/cron.daily 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.4 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct user to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/cron.daily/ -maxdepth 0 -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_cron_daily_newown variable if represented by uid ansible.builtin.set_fact: file_owner_cron_daily_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /etc/cron.daily/ ansible.builtin.file: path: /etc/cron.daily/ follow: false state: directory owner: '{{ file_owner_cron_daily_newown }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Owner on cron.hourly To properly set the owner of /etc/cron.hourly, run the command: $ sudo chown root /etc/cron.hourly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.3 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct user to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/cron.hourly/ -maxdepth 0 -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_cron_hourly_newown variable if represented by uid ansible.builtin.set_fact: file_owner_cron_hourly_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /etc/cron.hourly/ ansible.builtin.file: path: /etc/cron.hourly/ follow: false state: directory owner: '{{ file_owner_cron_hourly_newown }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Owner on cron.monthly To properly set the owner of /etc/cron.monthly, run the command: $ sudo chown root /etc/cron.monthly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.6 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct user to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/cron.monthly/ -maxdepth 0 -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_cron_monthly_newown variable if represented by uid ansible.builtin.set_fact: file_owner_cron_monthly_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /etc/cron.monthly/ ansible.builtin.file: path: /etc/cron.monthly/ follow: false state: directory owner: '{{ file_owner_cron_monthly_newown }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Owner on cron.weekly To properly set the owner of /etc/cron.weekly, run the command: $ sudo chown root /etc/cron.weekly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.5 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct user to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/cron.weekly/ -maxdepth 0 -type d ! -user 0 -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_cron_weekly_newown variable if represented by uid ansible.builtin.set_fact: file_owner_cron_weekly_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on directory /etc/cron.weekly/ ansible.builtin.file: path: /etc/cron.weekly/ follow: false state: directory owner: '{{ file_owner_cron_weekly_newown }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Owner on crontab To properly set the owner of /etc/crontab, run the command: $ sudo chown root /etc/crontab 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.2 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct user to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/crontab" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/crontab fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_crontab_newown variable if represented by uid ansible.builtin.set_fact: file_owner_crontab_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/crontab ansible.builtin.stat: path: /etc/crontab register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/crontab ansible.builtin.file: path: /etc/crontab follow: false owner: '{{ file_owner_crontab_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on cron.d To properly set the permissions of /etc/cron.d, run the command: $ sudo chmod 0700 /etc/cron.d 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.8 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should have the correct access rights to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then find -H /etc/cron.d/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d -exec chmod u-s,g-xwrs,o-xwrt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/cron.d/ file(s) ansible.builtin.command: 'find -P /etc/cron.d/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d ' register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/cron.d/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-xwrs,o-xwrt state: directory with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_d - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on cron.daily To properly set the permissions of /etc/cron.daily, run the command: $ sudo chmod 0700 /etc/cron.daily 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.4 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should have the correct access rights to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then find -H /etc/cron.daily/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d -exec chmod u-s,g-xwrs,o-xwrt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/cron.daily/ file(s) ansible.builtin.command: 'find -P /etc/cron.daily/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d ' register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/cron.daily/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-xwrs,o-xwrt state: directory with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_daily - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on cron.hourly To properly set the permissions of /etc/cron.hourly, run the command: $ sudo chmod 0700 /etc/cron.hourly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.3 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should have the correct access rights to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then find -H /etc/cron.hourly/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d -exec chmod u-s,g-xwrs,o-xwrt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/cron.hourly/ file(s) ansible.builtin.command: 'find -P /etc/cron.hourly/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d ' register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/cron.hourly/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-xwrs,o-xwrt state: directory with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_hourly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on cron.monthly To properly set the permissions of /etc/cron.monthly, run the command: $ sudo chmod 0700 /etc/cron.monthly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.6 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should have the correct access rights to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then find -H /etc/cron.monthly/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d -exec chmod u-s,g-xwrs,o-xwrt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/cron.monthly/ file(s) ansible.builtin.command: 'find -P /etc/cron.monthly/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d ' register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/cron.monthly/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-xwrs,o-xwrt state: directory with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_monthly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on cron.weekly To properly set the permissions of /etc/cron.weekly, run the command: $ sudo chmod 0700 /etc/cron.weekly 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.5 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should have the correct access rights to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then find -H /etc/cron.weekly/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d -exec chmod u-s,g-xwrs,o-xwrt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/cron.weekly/ file(s) ansible.builtin.command: 'find -P /etc/cron.weekly/ -maxdepth 0 -perm /u+s,g+xwrs,o+xwrt -type d ' register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/cron.weekly/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-s,g-xwrs,o-xwrt state: directory with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_weekly - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on crontab To properly set the permissions of /etc/crontab, run the command: $ sudo chmod 0600 /etc/crontab 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.2 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should have the correct access rights to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then chmod u-xs,g-xwrs,o-xwrt /etc/crontab else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/crontab ansible.builtin.stat: path: /etc/crontab register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xwrs,o-xwrt on /etc/crontab ansible.builtin.file: path: /etc/crontab mode: u-xs,g-xwrs,o-xwrt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed Restrict at and cron to Authorized Users if Necessary The /etc/cron.allow and /etc/at.allow files contain lists of users who are allowed to use cron and at to delay execution of processes. If these files exist and if the corresponding files /etc/cron.deny and /etc/at.deny do not exist, then only users listed in the relevant allow files can run the crontab and at commands to submit jobs to be run at scheduled intervals. On many systems, only the system administrator needs the ability to schedule jobs. Note that even if a given user is not listed in cron.allow, cron jobs can still be run as that user. The cron.allow file controls only administrative access to the crontab command for scheduling and modifying cron jobs. To restrict at and cron to only authorized users: Remove the cron.deny file:$ sudo rm /etc/cron.deny Edit /etc/cron.allow, adding one line for each user allowed to use the crontab command to create cron jobs.Remove the at.deny file:$ sudo rm /etc/at.deny Edit /etc/at.allow, adding one line for each user allowed to use the at command to create at jobs. Ensure that /etc/at.deny does not exist The file /etc/at.deny should not exist. Use /etc/at.allow instead. 2.4.2.1 2.2.6 2.2 Access to at should be restricted. It is easier to manage an allow list than a deny list. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if [[ -f /etc/at.deny ]]; then rm /etc/at.deny fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - disable_strategy - file_at_deny_not_exist - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Remove /etc/at.deny ansible.builtin.file: path: /etc/at.deny state: absent when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - disable_strategy - file_at_deny_not_exist - low_complexity - low_disruption - medium_severity - no_reboot_needed Ensure that /etc/cron.allow exists The file /etc/cron.allow should exist and should be used instead of /etc/cron.deny. 2.4.1.9 Access to crontab should be restricted. It is easier to manage an allow list than a deny list. Therefore, /etc/cron.allow needs to be created and used instead of /etc/cron.deny. Regardless of the existence of any of these files, the root administrative user is always allowed to setup a crontab. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then touch /etc/cron.allow chown 0 /etc/cron.allow chmod 0600 /etc/cron.allow else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - file_cron_allow_exists - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Add empty /etc/cron.allow ansible.builtin.file: path: /etc/cron.allow state: touch owner: '0' mode: '0600' when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - file_cron_allow_exists - low_complexity - low_disruption - medium_severity - no_reboot_needed Ensure that /etc/cron.deny does not exist The file /etc/cron.deny should not exist. Use /etc/cron.allow instead. 2.4.1.9 2.2.6 2.2 Access to cron should be restricted. It is easier to manage an allow list than a deny list. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if [[ -f /etc/cron.deny ]]; then rm /etc/cron.deny fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - disable_strategy - file_cron_deny_not_exist - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Remove /etc/cron.deny ansible.builtin.file: path: /etc/cron.deny state: absent when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - disable_strategy - file_cron_deny_not_exist - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns /etc/at.allow file If /etc/at.allow exists, it must be group-owned by root. To properly set the group owner of /etc/at.allow, run the command: $ sudo chgrp root /etc/at.allow 2.4.2.1 2.2.6 2.2 If the owner of the at.allow file is not set to root, the possibility exists for an unauthorized user to view or edit sensitive information. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/at.allow" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/at.allow fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_at_allow_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_at_allow_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/at.allow ansible.builtin.stat: path: /etc/at.allow register: file_exists when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/at.allow ansible.builtin.file: path: /etc/at.allow follow: false group: '{{ file_groupowner_at_allow_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Who Owns /etc/cron.allow file If /etc/cron.allow exists, it must be group-owned by root. To properly set the group owner of /etc/cron.allow, run the command: $ sudo chgrp root /etc/cron.allow 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.9 2.2.6 2.2 If the owner of the cron.allow file is not set to root, the possibility exists for an unauthorized user to view or edit sensitive information. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/cron.allow" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/cron.allow fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_cron_allow_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_cron_allow_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/cron.allow ansible.builtin.stat: path: /etc/cron.allow register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/cron.allow ansible.builtin.file: path: /etc/cron.allow follow: false group: '{{ file_groupowner_cron_allow_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_groupowner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns /etc/at.allow file If /etc/at.allow exists, it must be owned by root. To properly set the owner of /etc/at.allow, run the command: $ sudo chown root /etc/at.allow 2.4.2.1 2.2.6 2.2 If the owner of the at.allow file is not set to root, the possibility exists for an unauthorized user to view or edit sensitive information. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/at.allow" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/at.allow fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_at_allow_newown variable if represented by uid ansible.builtin.set_fact: file_owner_at_allow_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/at.allow ansible.builtin.stat: path: /etc/at.allow register: file_exists when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/at.allow ansible.builtin.file: path: /etc/at.allow follow: false owner: '{{ file_owner_at_allow_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify User Who Owns /etc/cron.allow file If /etc/cron.allow exists, it must be owned by root. To properly set the owner of /etc/cron.allow, run the command: $ sudo chown root /etc/cron.allow 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 2.4.1.9 2.2.6 2.2 If the owner of the cron.allow file is not set to root, the possibility exists for an unauthorized user to view or edit sensitive information. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/cron.allow" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/cron.allow fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_cron_allow_newown variable if represented by uid ansible.builtin.set_fact: file_owner_cron_allow_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/cron.allow ansible.builtin.stat: path: /etc/cron.allow register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/cron.allow ansible.builtin.file: path: /etc/cron.allow follow: false owner: '{{ file_owner_cron_allow_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_owner_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /etc/at.allow file If /etc/at.allow exists, it must have permissions 0640 or more restrictive. To properly set the permissions of /etc/at.allow, run the command: $ sudo chmod 0640 /etc/at.allow 2.4.2.1 2.2.6 2.2 If the permissions of the at.allow file are not set to 0640 or more restrictive, the possibility exists for an unauthorized user to view or edit sensitive information. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then chmod u-xs,g-xws,o-xwrt /etc/at.allow else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/at.allow ansible.builtin.stat: path: /etc/at.allow register: file_exists when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwrt on /etc/at.allow ansible.builtin.file: path: /etc/at.allow mode: u-xs,g-xws,o-xwrt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_at_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /etc/cron.allow file If /etc/cron.allow exists, it must have permissions 0640 or more restrictive. To properly set the permissions of /etc/cron.allow, run the command: $ sudo chmod 0640 /etc/cron.allow SRG-OS-000480-GPOS-00227 2.4.1.9 2.2.6 2.2 If the permissions of the cron.allow file are not set to 0640 or more restrictive, the possibility exists for an unauthorized user to view or edit sensitive information. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then chmod u-xs,g-xws,o-xwrt /etc/cron.allow else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/cron.allow ansible.builtin.stat: path: /etc/cron.allow register: file_exists when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwrt on /etc/cron.allow ansible.builtin.file: path: /etc/cron.allow mode: u-xs,g-xws,o-xwrt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_cron_allow - low_complexity - low_disruption - medium_severity - no_reboot_needed Deprecated services Some deprecated software services impact the overall system security due to their behavior (leak of confidentiality in network exchange, usage as uncontrolled communication channel, risk associated with the service due to its old age, etc. Uninstall the inet-based telnet server The inet-based telnet daemon should be uninstalled. 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 telnet allows clear text communications, and does not protect any data transmission between client and server. Any confidential data can be listened and no integrity checking is made. # CAUTION: This remediation script will remove inetutils-telnetd # from the system, and may remove any packages # that depend on inetutils-telnetd. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "inetutils-telnetd" ; then dnf remove -y --noautoremove "inetutils-telnetd" fi - name: 'Uninstall the inet-based telnet server: Ensure inetutils-telnetd is removed' ansible.builtin.package: name: inetutils-telnetd state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_inetutils-telnetd_removed include remove_inetutils-telnetd class remove_inetutils-telnetd { package { 'inetutils-telnetd': ensure => 'purged', } } package --remove=inetutils-telnetd package remove inetutils-telnetd dnf remove inetutils-telnetd Uninstall the nis package The support for Yellowpages should not be installed unless it is required. NIS is the historical SUN service for central account management, more and more replaced by LDAP. NIS does not support efficiently security constraints, ACL, etc. and should not be used. # CAUTION: This remediation script will remove nis # from the system, and may remove any packages # that depend on nis. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "nis" ; then dnf remove -y --noautoremove "nis" fi - name: 'Uninstall the nis package: Ensure nis is removed' ansible.builtin.package: name: nis state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_nis_removed include remove_nis class remove_nis { package { 'nis': ensure => 'purged', } } package --remove=nis package remove nis dnf remove nis Uninstall the ntpdate package ntpdate is a historical ntp synchronization client for unixes. It sould be uninstalled. ntpdate is an old not security-compliant ntp client. It should be replaced by modern ntp clients such as ntpd, able to use cryptographic mechanisms integrated in NTP. # CAUTION: This remediation script will remove ntpdate # from the system, and may remove any packages # that depend on ntpdate. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "ntpdate" ; then dnf remove -y --noautoremove "ntpdate" fi - name: 'Uninstall the ntpdate package: Ensure ntpdate is removed' ansible.builtin.package: name: ntpdate state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_ntpdate_removed include remove_ntpdate class remove_ntpdate { package { 'ntpdate': ensure => 'purged', } } package --remove=ntpdate package remove ntpdate dnf remove ntpdate Uninstall the ssl compliant telnet server The telnet daemon, even with ssl support, should be uninstalled. 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 telnet, even with ssl support, should not be installed. When remote shell is required, up-to-date ssh daemon can be used. # CAUTION: This remediation script will remove telnetd-ssl # from the system, and may remove any packages # that depend on telnetd-ssl. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "telnetd-ssl" ; then dnf remove -y --noautoremove "telnetd-ssl" fi - name: 'Uninstall the ssl compliant telnet server: Ensure telnetd-ssl is removed' ansible.builtin.package: name: telnetd-ssl state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_telnetd-ssl_removed include remove_telnetd-ssl class remove_telnetd-ssl { package { 'telnetd-ssl': ensure => 'purged', } } package --remove=telnetd-ssl package remove telnetd-ssl dnf remove telnetd-ssl Uninstall the telnet server The telnet daemon should be uninstalled. 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 telnet allows clear text communications, and does not protect any data transmission between client and server. Any confidential data can be listened and no integrity checking is made.' # CAUTION: This remediation script will remove telnetd # from the system, and may remove any packages # that depend on telnetd. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "telnetd" ; then dnf remove -y --noautoremove "telnetd" fi - name: 'Uninstall the telnet server: Ensure telnetd is removed' ansible.builtin.package: name: telnetd state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_telnetd_removed include remove_telnetd class remove_telnetd { package { 'telnetd': ensure => 'purged', } } package --remove=telnetd package remove telnetd dnf remove telnetd DHCP The Dynamic Host Configuration Protocol (DHCP) allows systems to request and obtain an IP address and other configuration parameters from a server. This guide recommends configuring networking on clients by manually editing the appropriate files under /etc/sysconfig. Use of DHCP can make client systems vulnerable to compromise by rogue DHCP servers, and should be avoided unless necessary. If using DHCP is necessary, however, there are best practices that should be followed to minimize security risk. Configure DHCP Client if Necessary If DHCP must be used, then certain configuration changes can minimize the amount of information it receives and applies from the network, and thus the amount of incorrect information a rogue DHCP server could successfully distribute. For more information on configuring dhclient, see the dhclient(8) and dhclient.conf(5) man pages. Minimize the DHCP-Configured Options Create the file /etc/dhcp/dhclient.conf, and add an appropriate setting for each of the ten configuration settings which can be obtained via DHCP. For each setting, do one of the following: If the setting should not be configured remotely by the DHCP server, select an appropriate static value, and add the line: supersede setting value; If the setting should be configured remotely by the DHCP server, add the lines: request setting; require setting; For example, suppose the DHCP server should provide only the IP address itself and the subnet mask. Then the entire file should look like: supersede domain-name "example.com"; supersede domain-name-servers 192.168.1.2; supersede nis-domain ""; supersede nis-servers ""; supersede ntp-servers "ntp.example.com "; supersede routers 192.168.1.1; supersede time-offset -18000; request subnet-mask; require subnet-mask; In this example, the options nis-servers and nis-domain are set to empty strings, on the assumption that the deprecated NIS protocol is not in use. It is necessary to supersede settings for unused services so that they cannot be set by a hostile DHCP server. If an option is set to an empty string, dhclient will typically not attempt to configure the service. By default, the DHCP client program, dhclient, requests and applies ten configuration options (in addition to the IP address) from the DHCP server. subnet-mask, broadcast-address, time-offset, routers, domain-name, domain-name-servers, host-name, nis-domain, nis-servers, and ntp-servers. Many of the options requested and applied by dhclient may be the same for every system on a network. It is recommended that almost all configuration options be assigned statically, and only options which must vary on a host-by-host basis be assigned via DHCP. This limits the damage which can be done by a rogue DHCP server. If appropriate for your site, it is also possible to supersede the host-name directive in /etc/dhcp/dhclient.conf, establishing a static hostname for the system. However, dhclient does not use the host name option provided by the DHCP server (instead using the value provided by a reverse DNS lookup). Configure DHCP Server If the system must act as a DHCP server, the configuration information it serves should be minimized. Also, support for other protocols and DNS-updating schemes should be explicitly disabled unless needed. The configuration file for dhcpd is called /etc/dhcp/dhcpd.conf. The file begins with a number of global configuration options. The remainder of the file is divided into sections, one for each block of addresses offered by dhcpd, each of which contains configuration options specific to that address block. Minimize Served Information Edit /etc/dhcp/dhcpd.conf. Examine each address range section within the file, and ensure that the following options are not defined unless there is an operational need to provide this information via DHCP: option domain-name option domain-name-servers option nis-domain option nis-servers option ntp-servers option routers option time-offset By default, the Red Hat Enterprise Linux client installation uses DHCP to request much of the above information from the DHCP server. In particular, domain-name, domain-name-servers, and routers are configured via DHCP. These settings are typically necessary for proper network functionality, but are also usually static across systems at a given site. 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 Because the configuration information provided by the DHCP server could be maliciously provided to clients by a rogue DHCP server, the amount of information provided via DHCP should be minimized. Remove these definitions from the DHCP server configuration to ensure that legitimate clients do not unnecessarily rely on DHCP for this information. Disable DHCP Server The DHCP server dhcpd is not installed or activated by default. If the software was installed and activated, but the system does not need to act as a DHCP server, it should be disabled and removed. Uninstall DHCP Server Package If the system does not need to act as a DHCP server, the dhcp package can be uninstalled. The dhcp package can be removed with the following command: $ sudo dnf remove dhcp 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 R62 2.2.4 2.2 Removing the DHCP server ensures that it cannot be easily or accidentally reactivated and disrupt network operation. # CAUTION: This remediation script will remove dhcp # from the system, and may remove any packages # that depend on dhcp. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "dhcp" ; then dnf remove -y --noautoremove "dhcp" fi - name: 'Uninstall DHCP Server Package: Ensure dhcp is removed' ansible.builtin.package: name: dhcp state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_dhcp_removed include remove_dhcp class remove_dhcp { package { 'dhcp': ensure => 'purged', } } package --remove=dhcp package remove dhcp dnf remove dhcp Uninstall kea Package If the system does not need to act as a DHCP server, the kea package can be uninstalled. R62 2.1.4 Removing the DHCP server ensures that it cannot be easily or accidentally reactivated and disrupt network operation. # CAUTION: This remediation script will remove kea # from the system, and may remove any packages # that depend on kea. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "kea" ; then dnf remove -y --noautoremove "kea" fi - name: 'Uninstall kea Package: Ensure kea is removed' ansible.builtin.package: name: kea state: absent tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_kea_removed include remove_kea class remove_kea { package { 'kea': ensure => 'purged', } } package --remove=kea package remove kea dnf remove kea DNS Server Most organizations have an operational need to run at least one nameserver. However, there are many common attacks involving DNS server software, and this server software should be disabled on any system on which it is not needed. Uninstall dnsmasq Package dnsmasq is a lightweight tool that provides DNS caching, DNS forwarding and DHCP (Dynamic Host Configuration Protocol) services. The dnsmasq package can be removed with the following command: $ sudo dnf remove dnsmasq 2.1.6 Unless a system is specifically designated to act as a DNS caching, DNS forwarding and/or DHCP server, it is recommended that the package be removed to reduce the potential attack surface. # CAUTION: This remediation script will remove dnsmasq # from the system, and may remove any packages # that depend on dnsmasq. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "dnsmasq" ; then dnf remove -y --noautoremove "dnsmasq" fi - name: 'Uninstall dnsmasq Package: Ensure dnsmasq is removed' ansible.builtin.package: name: dnsmasq state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_dnsmasq_removed include remove_dnsmasq class remove_dnsmasq { package { 'dnsmasq': ensure => 'purged', } } package --remove=dnsmasq package remove dnsmasq dnf remove dnsmasq Disable DNS Server DNS software should be disabled on any systems which does not need to be a nameserver. Note that the BIND DNS server software is not installed on Fedora by default. The remainder of this section discusses secure configuration of systems which must be nameservers. Uninstall bind Package The named service is provided by the bind package. The bind package can be removed with the following command: $ sudo dnf remove bind 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 2.1.5 If there is no need to make DNS server software available, removing it provides a safeguard against its activation. # CAUTION: This remediation script will remove bind # from the system, and may remove any packages # that depend on bind. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "bind" ; then dnf remove -y --noautoremove "bind" fi - name: 'Uninstall bind Package: Ensure bind is removed' ansible.builtin.package: name: bind state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_bind_removed include remove_bind class remove_bind { package { 'bind': ensure => 'purged', } } package --remove=bind package remove bind dnf remove bind Application Whitelisting Daemon Fapolicyd (File Access Policy Daemon) implements application whitelisting to decide file access rights. Applications that are known via a reputation source are allowed access while unknown applications are not. The daemon makes use of the kernel's fanotify interface to determine file access rights. Install fapolicyd Package The fapolicyd package can be installed with the following command: $ sudo dnf install fapolicyd CM-6(a) SI-4(22) FMT_SMF_EXT.1 SRG-OS-000370-GPOS-00155 SRG-OS-000368-GPOS-00154 SRG-OS-000480-GPOS-00230 fapolicyd (File Access Policy Daemon) implements application whitelisting to decide file access rights. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "fapolicyd" ; then dnf install -y "fapolicyd" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-4(22) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_fapolicyd_installed - name: Ensure fapolicyd is installed ansible.builtin.package: name: fapolicyd state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-SI-4(22) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_fapolicyd_installed include install_fapolicyd class install_fapolicyd { package { 'fapolicyd': ensure => 'installed', } } package --add=fapolicyd [[packages]] name = "fapolicyd" version = "*" package install fapolicyd dnf install fapolicyd fapolicyd Must be Configured to Limit Access to Users Home Folders fapolicyd needs be configured so that users cannot give access to their home folders to other users. This rule is deprecated and there is no replacement at this time. Previous versions of this rule provided fixtext that would cause fapolicyd not to start. CM-6 b SRG-OS-000480-GPOS-00230 Users' home directories/folders may contain information of a sensitive nature. Non-privileged users should coordinate any sharing of information with a System Administrator (SA) through shared resources. fapolicyd can confine users to their home directory, not allowing them to make any changes outside of their own home directories. Confining users to their home directory will minimize the risk of sharing information. FTP Server FTP is a common method for allowing remote access to files. Like telnet, the FTP protocol is unencrypted, which means that passwords and other data transmitted during the session can be captured and that the session is vulnerable to hijacking. Therefore, running the FTP server software is not recommended. However, there are some FTP server configurations which may be appropriate for some environments, particularly those which allow only read-only anonymous access as a means of downloading data available to the public. Remove ftp Package FTP (File Transfer Protocol) is a traditional and widely used standard tool for transferring files between a server and clients over a network, especially where no authentication is necessary (permits anonymous users to connect to a server). The ftp package can be removed with the following command: $ sudo dnf remove ftp 2.2.1 2.2.4 2.2 FTP does not protect the confidentiality of data or authentication credentials. It is recommended SFTP be used if file transfer is required. Unless there is a need to run the system as a FTP server (for example, to allow anonymous downloads), it is recommended that the package be removed to reduce the potential attack surface. # CAUTION: This remediation script will remove ftp # from the system, and may remove any packages # that depend on ftp. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "ftp" ; then dnf remove -y --noautoremove "ftp" fi - name: 'Remove ftp Package: Ensure ftp is removed' ansible.builtin.package: name: ftp state: absent tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_ftp_removed include remove_ftp class remove_ftp { package { 'ftp': ensure => 'purged', } } package --remove=ftp package remove ftp dnf remove ftp Disable vsftpd if Possible To minimize attack surface, disable vsftpd if at all possible. Uninstall vsftpd Package The vsftpd package can be removed with the following command: $ sudo dnf remove vsftpd 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) IA-5(1)(c) IA-5(1).1(v) CM-7 CM-7.1(ii) PR.IP-1 PR.PT-3 SRG-OS-000074-GPOS-00042 SRG-OS-000095-GPOS-00049 SRG-OS-000480-GPOS-00227 2.1.7 Removing the vsftpd package decreases the risk of its accidental activation. # CAUTION: This remediation script will remove vsftpd # from the system, and may remove any packages # that depend on vsftpd. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "vsftpd" ; then dnf remove -y --noautoremove "vsftpd" fi - name: 'Uninstall vsftpd Package: Ensure vsftpd is removed' ansible.builtin.package: name: vsftpd state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7 - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-CM-7.1(ii) - NIST-800-53-IA-5(1)(c) - NIST-800-53-IA-5(1).1(v) - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_vsftpd_removed include remove_vsftpd class remove_vsftpd { package { 'vsftpd': ensure => 'purged', } } package --remove=vsftpd package remove vsftpd dnf remove vsftpd Configure vsftpd to Provide FTP Service if Necessary The primary vsftpd configuration file is /etc/vsftpd.conf, if that file exists, or /etc/vsftpd/vsftpd.conf if it does not. Configure Firewalls to Protect the FTP Server By default, iptables blocks access to the ports used by the web server. To configure iptables to allow port 21 traffic, one must edit /etc/sysconfig/iptables and /etc/sysconfig/ip6tables (if IPv6 is in use). Add the following line, ensuring that it appears before the final LOG and DROP lines for the INPUT chain: -A INPUT -m state --state NEW -p tcp --dport 21 -j ACCEPT Edit the file /etc/sysconfig/iptables-config. Ensure that the space-separated list of modules contains the FTP connection tracking module: IPTABLES_MODULES="ip_conntrack_ftp" These settings configure the firewall to allow connections to an FTP server. Disable FTP Uploads if Possible Is there a mission-critical reason for users to upload files via FTP? If not, edit the vsftpd configuration file to add or correct the following configuration options: write_enable=NO If FTP uploads are necessary, follow the guidance in the remainder of this section to secure these transactions as much as possible. Anonymous FTP can be a convenient way to make files available for universal download. However, it is less common to have a need to allow unauthenticated users to place files on the FTP server. If this must be done, it is necessary to ensure that files cannot be uploaded and downloaded from the same directory. Place the FTP Home Directory on its Own Partition By default, the anonymous FTP root is the home directory of the FTP user account. The df command can be used to verify that this directory is on its own partition. If there is a mission-critical reason for anonymous users to upload files, precautions must be taken to prevent these users from filling a disk used by other services. Enable Logging of All FTP Transactions Add or correct the following configuration options within the vsftpd configuration file, located at /etc/vsftpd/vsftpd.conf: xferlog_enable=YES xferlog_std_format=NO log_ftp_protocol=YES If verbose logging to vsftpd.log is done, sparse logging of downloads to /var/log/xferlog will not also occur. However, the information about what files were downloaded is included in the information logged to vsftpd.log. To trace malicious activity facilitated by the FTP service, it must be configured to ensure that all commands sent to the FTP server are logged using the verbose vsftpd log format. The default vsftpd log file is /var/log/vsftpd.log. Create Warning Banners for All FTP Users Edit the vsftpd configuration file, which resides at /etc/vsftpd/vsftpd.conf by default. Add or correct the following configuration options: banner_file=/etc/issue This setting will cause the system greeting banner to be used for FTP connections as well. Restrict the Set of Users Allowed to Access FTP This section describes how to disable non-anonymous (password-based) FTP logins, or, if it is not possible to do this entirely due to legacy applications, how to restrict insecure FTP login to only those users who have an identified need for this access. Limit Users Allowed FTP Access if Necessary If there is a mission-critical reason for users to access their accounts via the insecure FTP protocol, limit the set of users who are allowed this access. Edit the vsftpd configuration file. Add or correct the following configuration options: userlist_enable=YES userlist_file=/etc/vsftp.ftpusers userlist_deny=NO Edit the file /etc/vsftp.ftpusers. For each user USERNAME who should be allowed to access the system via FTP, add a line containing that user's name: USERNAME If anonymous access is also required, add the anonymous usernames to /etc/vsftp.ftpusers as well. anonymous ftp Historically, the file /etc/ftpusers contained a list of users who were not allowed to access the system via FTP. It was used to prevent system users such as the root user from logging in via the insecure FTP protocol. However, when the configuration option userlist deny=NO is set, vsftpd interprets ftpusers as the set of users who are allowed to login via FTP. Since it should be possible for most users to access their accounts via secure protocols, it is recommended that this setting be used, so that non-anonymous FTP access can be limited to legacy users who have been explicitly identified. Restrict Access to Anonymous Users if Possible Is there a mission-critical reason for users to transfer files to/from their own accounts using FTP, rather than using a secure protocol like SCP/SFTP? If not, edit the vsftpd configuration file. Add or correct the following configuration option: local_enable=NO If non-anonymous FTP logins are necessary, follow the guidance in the remainder of this section to secure these logins as much as possible. 11 12 14 15 16 18 3 5 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) AC-3 AC-17(a) PR.AC-4 PR.AC-6 PR.IP-1 PR.PT-3 The use of non-anonymous FTP logins is strongly discouraged. Since SSH clients and servers are widely available, and since SSH provides support for a transfer mode which resembles FTP in user interface, there is no good reason to allow password-based FTP access.' Use vsftpd to Provide FTP Service if Necessary If your use-case requires FTP service, install and set-up vsftpd to provide it. Install vsftpd Package If this system must operate as an FTP server, install the vsftpd package via the standard channels. The vsftpd package can be installed with the following command: $ sudo dnf install vsftpd 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-6(a) PR.IP-1 PR.PT-3 After Red Hat Enterprise Linux 2.1, Red Hat switched from distributing wu-ftpd with Red Hat Enterprise Linux to distributing vsftpd. For security and for consistency with future Red Hat releases, the use of vsftpd is recommended. if ! rpm -q --quiet "vsftpd" ; then dnf install -y "vsftpd" fi - name: Ensure vsftpd is installed ansible.builtin.package: name: vsftpd state: present tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_vsftpd_installed include install_vsftpd class install_vsftpd { package { 'vsftpd': ensure => 'installed', } } package --add=vsftpd [[packages]] name = "vsftpd" version = "*" package install vsftpd dnf install vsftpd Web Server The web server is responsible for providing access to content via the HTTP protocol. Web servers represent a significant security risk because: The HTTP port is commonly probed by malicious sourcesWeb server software is very complex, and includes a long history of vulnerabilitiesThe HTTP protocol is unencrypted and vulnerable to passive monitoring The system's default web server software is Apache 2 and is provided in the RPM package httpd. Disable Apache if Possible If Apache was installed and activated, but the system does not need to act as a web server, then it should be disabled and removed from the system. Uninstall httpd Package The httpd package can be removed with the following command: $ sudo dnf remove httpd 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 2.1.19 If there is no need to make the web server software available, removing it provides a safeguard against its activation. # CAUTION: This remediation script will remove httpd # from the system, and may remove any packages # that depend on httpd. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "httpd" ; then dnf remove -y --noautoremove "httpd" fi - name: 'Uninstall httpd Package: Ensure httpd is removed' ansible.builtin.package: name: httpd state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_httpd_removed - unknown_severity include remove_httpd class remove_httpd { package { 'httpd': ensure => 'purged', } } package --remove=httpd package remove httpd dnf remove httpd Disable NGINX if Possible If NGINX was installed and activated, but the system does not need to act as a web server, then it should be removed from the system. Uninstall nginx Package The nginx package can be removed with the following command: $ sudo dnf remove nginx BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 2.1.19 If there is no need to make the web server software available, removing it provides a safeguard against its activation. # CAUTION: This remediation script will remove nginx # from the system, and may remove any packages # that depend on nginx. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "nginx" ; then dnf remove -y --noautoremove "nginx" fi - name: 'Uninstall nginx Package: Ensure nginx is removed' ansible.builtin.package: name: nginx state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_nginx_removed - unknown_severity include remove_nginx class remove_nginx { package { 'nginx': ensure => 'purged', } } package --remove=nginx package remove nginx dnf remove nginx IMAP and POP3 Server Dovecot provides IMAP and POP3 services. It is not installed by default. The project page at http://www.dovecot.org contains more detailed information about Dovecot configuration. Disable Cyrus IMAP If the system does not need to operate as an IMAP or POP3 server, the Cyrus IMAP software should be removed. Uninstall cyrus-imapd Package The cyrus-imapd package can be removed with the following command: $ sudo dnf remove cyrus-imapd 2.1.8 If there is no need to make the cyrus-imapd software available, removing it provides a safeguard against its activation. # CAUTION: This remediation script will remove cyrus-imapd # from the system, and may remove any packages # that depend on cyrus-imapd. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "cyrus-imapd" ; then dnf remove -y --noautoremove "cyrus-imapd" fi - name: 'Uninstall cyrus-imapd Package: Ensure cyrus-imapd is removed' ansible.builtin.package: name: cyrus-imapd state: absent tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_cyrus-imapd_removed - unknown_severity include remove_cyrus-imapd class remove_cyrus-imapd { package { 'cyrus-imapd': ensure => 'purged', } } package --remove=cyrus-imapd package remove cyrus-imapd dnf remove cyrus-imapd Disable Dovecot If the system does not need to operate as an IMAP or POP3 server, the dovecot software should be disabled and removed. Uninstall dovecot Package The dovecot package can be removed with the following command: $ sudo dnf remove dovecot 2.1.8 If there is no need to make the Dovecot software available, removing it provides a safeguard against its activation. # CAUTION: This remediation script will remove dovecot # from the system, and may remove any packages # that depend on dovecot. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "dovecot" ; then dnf remove -y --noautoremove "dovecot" fi - name: 'Uninstall dovecot Package: Ensure dovecot is removed' ansible.builtin.package: name: dovecot state: absent tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_dovecot_removed - unknown_severity include remove_dovecot class remove_dovecot { package { 'dovecot': ensure => 'purged', } } package --remove=dovecot package remove dovecot dnf remove dovecot LDAP LDAP is a popular directory service, that is, a standardized way of looking up information from a central database. Fedora includes software that enables a system to act as both an LDAP client and server. Configure OpenLDAP Clients This section provides information on which security settings are important to configure in OpenLDAP clients by manually editing the appropriate configuration files. Fedora provides an automated configuration tool called authconfig and a graphical wrapper for authconfig called system-config-authentication. However, these tools do not provide as much control over configuration as manual editing of configuration files. The authconfig tools do not allow you to specify locations of SSL certificate files, which is useful when trying to use SSL cleanly across several protocols. Installation and configuration of OpenLDAP on Fedora is available at Before configuring any system to be an LDAP client, ensure that a working LDAP server is present on the network. Ensure LDAP client is not installed The Lightweight Directory Access Protocol (LDAP) is a service that provides a method for looking up information from a central database. The openldap-clients package can be removed with the following command: $ sudo dnf remove openldap-clients 2.2.2 If the system does not need to act as an LDAP client, it is recommended that the software is removed to reduce the potential attack surface. # CAUTION: This remediation script will remove openldap-clients # from the system, and may remove any packages # that depend on openldap-clients. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "openldap-clients" ; then dnf remove -y --noautoremove "openldap-clients" fi - name: 'Ensure LDAP client is not installed: Ensure openldap-clients is removed' ansible.builtin.package: name: openldap-clients state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_openldap-clients_removed include remove_openldap-clients class remove_openldap-clients { package { 'openldap-clients': ensure => 'purged', } } package --remove=openldap-clients package remove openldap-clients dnf remove openldap-clients Mail Server Software Mail servers are used to send and receive email over the network. Mail is a very common service, and Mail Transfer Agents (MTAs) are obvious targets of network attack. Ensure that systems are not running MTAs unnecessarily, and configure needed MTAs as defensively as possible. Very few systems at any site should be configured to directly receive email over the network. Users should instead use mail client programs to retrieve email from a central server that supports protocols such as IMAP or POP3. However, it is normal for most systems to be independently capable of sending email, for instance so that cron jobs can report output to an administrator. Most MTAs, including Postfix, support a submission-only mode in which mail can be sent from the local system to a central site MTA (or directly delivered to a local account), but the system still cannot receive mail directly over a network. The alternatives program in Fedora permits selection of other mail server software (such as Sendmail), but Postfix is the default and is preferred. Postfix was coded with security in mind and can also be more effectively contained by SELinux as its modular design has resulted in separate processes performing specific actions. More information is available on its website, http://www.postfix.org. The Postfix package is installed A mail server is required for sending emails. The postfix package can be installed with the following command: $ sudo dnf install postfix SRG-OS-000046-GPOS-00022 Emails can be used to notify designated personnel about important system events such as failures or warnings. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "postfix" ; then dnf install -y "postfix" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_postfix_installed - name: Ensure postfix is installed ansible.builtin.package: name: postfix state: present when: '"kernel" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_postfix_installed include install_postfix class install_postfix { package { 'postfix': ensure => 'installed', } } package --add=postfix [[packages]] name = "postfix" version = "*" package install postfix dnf install postfix Uninstall Sendmail Package Sendmail is not the default mail transfer agent and is not installed by default. The sendmail package can be removed with the following command: $ sudo dnf remove sendmail 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 SRG-OS-000095-GPOS-00049 R62 The sendmail software was not developed with security in mind and its design prevents it from being effectively contained by SELinux. Postfix should be used instead. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # CAUTION: This remediation script will remove sendmail # from the system, and may remove any packages # that depend on sendmail. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "sendmail" ; then dnf remove -y --noautoremove "sendmail" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_sendmail_removed - name: 'Uninstall Sendmail Package: Ensure sendmail is removed' ansible.builtin.package: name: sendmail state: absent when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_sendmail_removed include remove_sendmail class remove_sendmail { package { 'sendmail': ensure => 'purged', } } package --remove=sendmail package remove sendmail dnf remove sendmail Ensure Mail Transfer Agent is not Listening on any non-loopback Address Mail Transfer Agents (MTA), such as sendmail and Postfix, are used to listen for incoming mail and transfer the messages to the appropriate user or mail server. If the system is not intended to be a mail server, it is recommended that the MTA be configured to only process local mail. 2.1.23 The software for all Mail Transfer Agents is complex and most have a long history of security issues. While it is important to ensure that the system can process local mail messages, it is not necessary to have the MTA's daemon listening on a port unless the server is intended to be a mail server that receives and processes mail from other systems. Configure SMTP For Mail Clients This section discusses settings for Postfix in a submission-only e-mail configuration. Postfix Network Interfaces The setting for inet_interfaces in /etc/postfix/main.cf loopback-only loopback-only localhost Postfix relayhost Specify the host all outbound email should be routed into. smtp.$mydomain Postfix Root Mail Alias Specify an email address (string) for a root mail alias. change_me@localhost system.administrator@mail.mil Configure System to Forward All Mail For The Root Account Make sure that mails delivered to root user are forwarded to a monitored email address. Make sure that the address is a valid email address reachable from the system in question. Use the following command to configure the alias: $ sudo echo "root: " >> /etc/aliases $ sudo newaliases CM-6(a) SRG-OS-000046-GPOS-00022 R75 A number of system services utilize email messages sent to the root user to notify system administrators of active or impending issues. These messages must be forwarded to at least one monitored email address. Configure System to Forward All Mail From Postmaster to The Root Account Verify the administrators are notified in the event of an audit processing failure. Check that the "/etc/aliases" file has a defined value for "root". $ sudo grep "postmaster:\s*root$" /etc/aliases postmaster: root AU-5(a) AU-5.1(ii) SRG-OS-000046-GPOS-00022 It is critical for the appropriate personnel to be aware if a system is at risk of failing to process audit logs as required. Without this notification, the security personnel may be unaware of an impending failure of the audit capability, and system operation may be adversely affected. Audit processing failures include software/hardware errors, failures in the audit capturing mechanisms, and audit storage capacity being reached or exceeded. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if [ -e "/etc/aliases" ] ; then LC_ALL=C sed -i "/^\s*postmaster\s*:\s*/Id" "/etc/aliases" else touch "/etc/aliases" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/aliases" cp "/etc/aliases" "/etc/aliases.bak" # Insert at the end of the file printf '%s\n' "postmaster: root" >> "/etc/aliases" # Clean up after ourselves. rm "/etc/aliases.bak" if [ -f /usr/bin/newaliases ]; then newaliases fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(a) - NIST-800-53-AU-5.1(ii) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - postfix_client_configure_mail_alias_postmaster - name: Configure System to Forward All Mail From Postmaster to The Root Account block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/aliases create: true regexp: (?i)^\s*postmaster\s*:\s* state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/aliases ansible.builtin.lineinfile: path: /etc/aliases create: true regexp: (?i)^\s*postmaster\s*:\s* state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/aliases ansible.builtin.lineinfile: path: /etc/aliases create: true regexp: (?i)^\s*postmaster\s*:\s* line: 'postmaster: root' state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(a) - NIST-800-53-AU-5.1(ii) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - postfix_client_configure_mail_alias_postmaster - name: Check if newaliases command is available ansible.builtin.stat: path: /usr/bin/newaliases register: result_newaliases_present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(a) - NIST-800-53-AU-5.1(ii) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - postfix_client_configure_mail_alias_postmaster - name: Update postfix aliases ansible.builtin.command: cmd: newaliases when: - '"kernel" in ansible_facts.packages' - result_newaliases_present.stat.exists tags: - NIST-800-53-AU-5(a) - NIST-800-53-AU-5.1(ii) - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - postfix_client_configure_mail_alias_postmaster Configure System to Forward All Mail through a specific host Set up a relay host that will act as a gateway for all outbound email. Edit the file /etc/postfix/main.cf to ensure that only the following relayhost line appears: relayhost = A central outbound email location ensures messages sent from any network host can be audited for potential unexpected content. Tooling on the central server may help prevent spam or viruses from being delivered. Disable Postfix Network Listening Edit the file /etc/postfix/main.cf to ensure that only the following inet_interfaces line appears: inet_interfaces = 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 R74 2.1.23 1.4.2 1.4 This ensures postfix accepts mail messages (such as cron job reports) from the local system only, and not from the network, which protects it from network attack. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q postfix; }; then var_postfix_inet_interfaces='' if [ -e "/etc/postfix/main.cf" ] ; then LC_ALL=C sed -i "/^\s*inet_interfaces\s\+=\s\+/Id" "/etc/postfix/main.cf" else touch "/etc/postfix/main.cf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/postfix/main.cf" cp "/etc/postfix/main.cf" "/etc/postfix/main.cf.bak" # Insert at the end of the file printf '%s\n' "inet_interfaces=$var_postfix_inet_interfaces" >> "/etc/postfix/main.cf" # Clean up after ourselves. rm "/etc/postfix/main.cf.bak" systemctl restart postfix else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - postfix_network_listening_disabled - restrict_strategy - name: XCCDF Value var_postfix_inet_interfaces # promote to variable set_fact: var_postfix_inet_interfaces: !!str tags: - always - name: Gather list of packages ansible.builtin.package_facts: manager: auto when: - '"kernel" in ansible_facts.packages' - '"postfix" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - postfix_network_listening_disabled - restrict_strategy - name: Make changes to Postfix configuration file ansible.builtin.lineinfile: path: /etc/postfix/main.cf create: false regexp: (?i)^inet_interfaces\s*=\s.* line: inet_interfaces = {{ var_postfix_inet_interfaces }} state: present insertafter: ^inet_interfaces\s*=\s.* when: - '"kernel" in ansible_facts.packages' - '"postfix" in ansible_facts.packages' - '"postfix" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-1.4 - PCI-DSSv4-1.4.2 - low_complexity - low_disruption - medium_severity - no_reboot_needed - postfix_network_listening_disabled - restrict_strategy NFS and RPC The Network File System is a popular distributed filesystem for the Unix environment, and is very widely deployed. This section discusses the circumstances under which it is possible to disable NFS and its dependencies, and then details steps which should be taken to secure NFS's configuration. This section is relevant to systems operating as NFS clients, as well as to those operating as NFS servers. Uninstall nfs-utils Package The nfs-utils package can be removed with the following command: $ sudo dnf remove nfs-utils SRG-OS-000095-GPOS-00049 nfs-utils provides a daemon for the kernel NFS server and related tools. This package also contains the showmount program. showmount queries the mount daemon on a remote host for information about the Network File System (NFS) server on the remote host. For example, showmount can display the clients which are mounted on that host. # CAUTION: This remediation script will remove nfs-utils # from the system, and may remove any packages # that depend on nfs-utils. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "nfs-utils" ; then dnf remove -y --noautoremove "nfs-utils" fi - name: 'Uninstall nfs-utils Package: Ensure nfs-utils is removed' ansible.builtin.package: name: nfs-utils state: absent tags: - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_nfs-utils_removed include remove_nfs-utils class remove_nfs-utils { package { 'nfs-utils': ensure => 'purged', } } package --remove=nfs-utils package remove nfs-utils dnf remove nfs-utils Disable All NFS Services if Possible If there is not a reason for the system to operate as either an NFS client or an NFS server, follow all instructions in this section to disable subsystems required by NFS. The steps in this section will prevent a system from operating as either an NFS client or an NFS server. Only perform these steps on systems which do not need NFS at all. Disable netfs if Possible To determine if any network filesystems handled by netfs are currently mounted on the system execute the following command: $ mount -t nfs,nfs4,smbfs,cifs,ncpfs If the command did not return any output then disable netfs. Disable Network File Systems (netfs) The netfs script manages the boot-time mounting of several types of networked filesystems, of which NFS and Samba are the most common. If these filesystem types are not in use, the script can be disabled, protecting the system somewhat against accidental or malicious changes to /etc/fstab and against flaws in the netfs script itself. The netfs service can be disabled with the following command: $ sudo systemctl mask --now netfs.service # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'netfs.service' fi "$SYSTEMCTL_EXEC" disable 'netfs.service' "$SYSTEMCTL_EXEC" mask 'netfs.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files netfs.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'netfs.socket' fi "$SYSTEMCTL_EXEC" mask 'netfs.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'netfs.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_netfs_disabled - unknown_severity - name: Disable Network File Systems (netfs) - Disable service netfs block: - name: Disable Network File Systems (netfs) - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable Network File Systems (netfs) - Ensure netfs.service is Masked ansible.builtin.systemd: name: netfs.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("netfs.service", multiline=True) - name: Unit Socket Exists - netfs.socket ansible.builtin.command: systemctl -q list-unit-files netfs.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable Network File Systems (netfs) - Disable Socket netfs ansible.builtin.systemd: name: netfs.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("netfs.socket", multiline=True) tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_netfs_disabled - special_service_block - unknown_severity when: '"kernel" in ansible_facts.packages' include disable_netfs class disable_netfs { service {'netfs': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: netfs.service enabled: false mask: true - name: netfs.socket enabled: false mask: true [customizations.services] masked = ["netfs"] service disable netfs Disable Services Used Only by NFS If NFS is not needed, disable the NFS client daemons nfslock, rpcgssd, and rpcidmapd. All of these daemons run with elevated privileges, and many listen for network connections. If they are not needed, they should be disabled to improve system security posture. Disable Network File System Lock Service (nfslock) The Network File System Lock (nfslock) service starts the required remote procedure call (RPC) processes which allow clients to lock files on the server. If the local system is not configured to mount NFS filesystems then this service should be disabled. The nfslock service can be disabled with the following command: $ sudo systemctl mask --now nfslock.service # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'nfslock.service' fi "$SYSTEMCTL_EXEC" disable 'nfslock.service' "$SYSTEMCTL_EXEC" mask 'nfslock.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files nfslock.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'nfslock.socket' fi "$SYSTEMCTL_EXEC" mask 'nfslock.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'nfslock.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_nfslock_disabled - unknown_severity - name: Disable Network File System Lock Service (nfslock) - Disable service nfslock block: - name: Disable Network File System Lock Service (nfslock) - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable Network File System Lock Service (nfslock) - Ensure nfslock.service is Masked ansible.builtin.systemd: name: nfslock.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("nfslock.service", multiline=True) - name: Unit Socket Exists - nfslock.socket ansible.builtin.command: systemctl -q list-unit-files nfslock.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable Network File System Lock Service (nfslock) - Disable Socket nfslock ansible.builtin.systemd: name: nfslock.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("nfslock.socket", multiline=True) tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_nfslock_disabled - special_service_block - unknown_severity when: '"kernel" in ansible_facts.packages' include disable_nfslock class disable_nfslock { service {'nfslock': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: nfslock.service enabled: false mask: true - name: nfslock.socket enabled: false mask: true [customizations.services] masked = ["nfslock"] service disable nfslock Disable rpcbind Service The rpcbind utility maps RPC services to the ports on which they listen. RPC processes notify rpcbind when they start, registering the ports they are listening on and the RPC program numbers they expect to serve. The rpcbind service redirects the client to the proper port number so it can communicate with the requested service. If the system does not require RPC (such as for NFS servers) then this service should be disabled. The rpcbind service can be disabled with the following command: $ sudo systemctl mask --now rpcbind.service 2.1.12 2.2.4 2.2 If the system does not require rpc based services, it is recommended that rpcbind be disabled to reduce the attack surface. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcbind.service' fi "$SYSTEMCTL_EXEC" disable 'rpcbind.service' "$SYSTEMCTL_EXEC" mask 'rpcbind.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files rpcbind.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcbind.socket' fi "$SYSTEMCTL_EXEC" mask 'rpcbind.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'rpcbind.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - service_rpcbind_disabled - name: Disable rpcbind Service - Disable service rpcbind block: - name: Disable rpcbind Service - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable rpcbind Service - Ensure rpcbind.service is Masked ansible.builtin.systemd: name: rpcbind.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("rpcbind.service", multiline=True) - name: Unit Socket Exists - rpcbind.socket ansible.builtin.command: systemctl -q list-unit-files rpcbind.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable rpcbind Service - Disable Socket rpcbind ansible.builtin.systemd: name: rpcbind.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("rpcbind.socket", multiline=True) tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - service_rpcbind_disabled - special_service_block when: '"kernel" in ansible_facts.packages' include disable_rpcbind class disable_rpcbind { service {'rpcbind': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: rpcbind.service enabled: false mask: true - name: rpcbind.socket enabled: false mask: true [customizations.services] masked = ["rpcbind"] service disable rpcbind Disable Secure RPC Client Service (rpcgssd) The rpcgssd service manages RPCSEC GSS contexts required to secure protocols that use RPC (most often Kerberos and NFS). The rpcgssd service is the client-side of RPCSEC GSS. If the system does not require secure RPC then this service should be disabled. The rpcgssd service can be disabled with the following command: $ sudo systemctl mask --now rpcgssd.service # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcgssd.service' fi "$SYSTEMCTL_EXEC" disable 'rpcgssd.service' "$SYSTEMCTL_EXEC" mask 'rpcgssd.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files rpcgssd.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcgssd.socket' fi "$SYSTEMCTL_EXEC" mask 'rpcgssd.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'rpcgssd.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_rpcgssd_disabled - unknown_severity - name: Disable Secure RPC Client Service (rpcgssd) - Disable service rpcgssd block: - name: Disable Secure RPC Client Service (rpcgssd) - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable Secure RPC Client Service (rpcgssd) - Ensure rpcgssd.service is Masked ansible.builtin.systemd: name: rpcgssd.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("rpcgssd.service", multiline=True) - name: Unit Socket Exists - rpcgssd.socket ansible.builtin.command: systemctl -q list-unit-files rpcgssd.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable Secure RPC Client Service (rpcgssd) - Disable Socket rpcgssd ansible.builtin.systemd: name: rpcgssd.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("rpcgssd.socket", multiline=True) tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_rpcgssd_disabled - special_service_block - unknown_severity when: '"kernel" in ansible_facts.packages' include disable_rpcgssd class disable_rpcgssd { service {'rpcgssd': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: rpcgssd.service enabled: false mask: true - name: rpcgssd.socket enabled: false mask: true [customizations.services] masked = ["rpcgssd"] service disable rpcgssd Disable RPC ID Mapping Service (rpcidmapd) The rpcidmapd service is used to map user names and groups to UID and GID numbers on NFSv4 mounts. If NFS is not in use on the local system then this service should be disabled. The rpcidmapd service can be disabled with the following command: $ sudo systemctl mask --now rpcidmapd.service # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcidmapd.service' fi "$SYSTEMCTL_EXEC" disable 'rpcidmapd.service' "$SYSTEMCTL_EXEC" mask 'rpcidmapd.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files rpcidmapd.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcidmapd.socket' fi "$SYSTEMCTL_EXEC" mask 'rpcidmapd.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'rpcidmapd.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_rpcidmapd_disabled - unknown_severity - name: Disable RPC ID Mapping Service (rpcidmapd) - Disable service rpcidmapd block: - name: Disable RPC ID Mapping Service (rpcidmapd) - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable RPC ID Mapping Service (rpcidmapd) - Ensure rpcidmapd.service is Masked ansible.builtin.systemd: name: rpcidmapd.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("rpcidmapd.service", multiline=True) - name: Unit Socket Exists - rpcidmapd.socket ansible.builtin.command: systemctl -q list-unit-files rpcidmapd.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable RPC ID Mapping Service (rpcidmapd) - Disable Socket rpcidmapd ansible.builtin.systemd: name: rpcidmapd.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("rpcidmapd.socket", multiline=True) tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_rpcidmapd_disabled - special_service_block - unknown_severity when: '"kernel" in ansible_facts.packages' include disable_rpcidmapd class disable_rpcidmapd { service {'rpcidmapd': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: rpcidmapd.service enabled: false mask: true - name: rpcidmapd.socket enabled: false mask: true [customizations.services] masked = ["rpcidmapd"] service disable rpcidmapd Configure All Systems which Use NFS The steps in this section are appropriate for all systems which run NFS, whether they operate as clients or as servers. Configure NFS Services to Use Fixed Ports (NFSv3 and NFSv2) Firewalling should be done at each host and at the border firewalls to protect the NFS daemons from remote access, since NFS servers should never be accessible from outside the organization. However, by default for NFSv3 and NFSv2, the RPC Bind service assigns each NFS service to a port dynamically at service startup time. Dynamic ports cannot be protected by port filtering firewalls such as iptables. Therefore, restrict each service to always use a given port, so that firewalling can be done effectively. Note that, because of the way RPC is implemented, it is not possible to disable the RPC Bind service even if ports are assigned statically to all RPC services. In NFSv4, the mounting and locking protocols have been incorporated into the protocol, and the server listens on the the well-known TCP port 2049. As such, NFSv4 does not need to interact with the rpcbind, lockd, and rpc.statd daemons, which can and should be disabled in a pure NFSv4 environment. The rpc.mountd daemon is still required on the NFS server to setup exports, but is not involved in any over-the-wire operations. Configure lockd to use static TCP port Configure the lockd daemon to use a static TCP port as opposed to letting the RPC Bind service dynamically assign a port. Edit the file /etc/sysconfig/nfs. Add or correct the following line: LOCKD_TCPPORT=lockd-port Where lockd-port is a port which is not used by any other service on your network. Restrict service to always use a given port, so that firewalling can be done effectively. Configure lockd to use static UDP port Configure the lockd daemon to use a static UDP port as opposed to letting the RPC Bind service dynamically assign a port. Edit the file /etc/sysconfig/nfs. Add or correct the following line: LOCKD_UDPPORT=lockd-port Where lockd-port is a port which is not used by any other service on your network. Restricting services to always use a given port enables firewalling to be done more effectively. Configure mountd to use static port Configure the mountd daemon to use a static port as opposed to letting the RPC Bind service dynamically assign a port. Edit the file /etc/sysconfig/nfs. Add or correct the following line: MOUNTD_PORT=statd-port Where mountd-port is a port which is not used by any other service on your network. Restricting services to always use a given port enables firewalling to be done more effectively. Configure statd to use static port Configure the statd daemon to use a static port as opposed to letting the RPC Bind service dynamically assign a port. Edit the file /etc/sysconfig/nfs. Add or correct the following line: STATD_PORT=statd-port Where statd-port is a port which is not used by any other service on your network. Restricting services to always use a given port enables firewalling to be done more effectively. Configure NFS Clients The steps in this section are appropriate for systems which operate as NFS clients. Disable NFS Server Daemons There is no need to run the NFS server daemons nfs and rpcsvcgssd except on a small number of properly secured systems designated as NFS servers. Ensure that these daemons are turned off on clients. Disable Network File System (nfs) The Network File System (NFS) service allows remote hosts to mount and interact with shared filesystems on the local system. If the local system is not designated as a NFS server then this service should be disabled. The nfs-server service can be disabled with the following command: $ sudo systemctl mask --now nfs-server.service 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) PR.AC-4 PR.AC-6 PR.PT-3 2.1.9 Unnecessary services should be disabled to decrease the attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'nfs-server.service' fi "$SYSTEMCTL_EXEC" disable 'nfs-server.service' "$SYSTEMCTL_EXEC" mask 'nfs-server.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files nfs-server.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'nfs-server.socket' fi "$SYSTEMCTL_EXEC" mask 'nfs-server.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'nfs-server.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_nfs_disabled - unknown_severity - name: Disable Network File System (nfs) - Disable service nfs-server block: - name: Disable Network File System (nfs) - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable Network File System (nfs) - Ensure nfs-server.service is Masked ansible.builtin.systemd: name: nfs-server.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("nfs-server.service", multiline=True) - name: Unit Socket Exists - nfs-server.socket ansible.builtin.command: systemctl -q list-unit-files nfs-server.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable Network File System (nfs) - Disable Socket nfs-server ansible.builtin.systemd: name: nfs-server.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("nfs-server.socket", multiline=True) tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_nfs_disabled - special_service_block - unknown_severity when: '"kernel" in ansible_facts.packages' include disable_nfs-server class disable_nfs-server { service {'nfs-server': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: nfs-server.service enabled: false mask: true - name: nfs-server.socket enabled: false mask: true [customizations.services] masked = ["nfs-server"] service disable nfs-server Disable Secure RPC Server Service (rpcsvcgssd) The rpcsvcgssd service manages RPCSEC GSS contexts required to secure protocols that use RPC (most often Kerberos and NFS). The rpcsvcgssd service is the server-side of RPCSEC GSS. If the system does not require secure RPC then this service should be disabled. The rpcsvcgssd service can be disabled with the following command: $ sudo systemctl mask --now rpcsvcgssd.service Unnecessary services should be disabled to decrease the attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcsvcgssd.service' fi "$SYSTEMCTL_EXEC" disable 'rpcsvcgssd.service' "$SYSTEMCTL_EXEC" mask 'rpcsvcgssd.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files rpcsvcgssd.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rpcsvcgssd.socket' fi "$SYSTEMCTL_EXEC" mask 'rpcsvcgssd.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'rpcsvcgssd.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_rpcsvcgssd_disabled - unknown_severity - name: Disable Secure RPC Server Service (rpcsvcgssd) - Disable service rpcsvcgssd block: - name: Disable Secure RPC Server Service (rpcsvcgssd) - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable Secure RPC Server Service (rpcsvcgssd) - Ensure rpcsvcgssd.service is Masked ansible.builtin.systemd: name: rpcsvcgssd.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("rpcsvcgssd.service", multiline=True) - name: Unit Socket Exists - rpcsvcgssd.socket ansible.builtin.command: systemctl -q list-unit-files rpcsvcgssd.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable Secure RPC Server Service (rpcsvcgssd) - Disable Socket rpcsvcgssd ansible.builtin.systemd: name: rpcsvcgssd.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("rpcsvcgssd.socket", multiline=True) tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_rpcsvcgssd_disabled - special_service_block - unknown_severity when: '"kernel" in ansible_facts.packages' include disable_rpcsvcgssd class disable_rpcsvcgssd { service {'rpcsvcgssd': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: rpcsvcgssd.service enabled: false mask: true - name: rpcsvcgssd.socket enabled: false mask: true [customizations.services] masked = ["rpcsvcgssd"] service disable rpcsvcgssd Specify UID and GID for Anonymous NFS Connections To specify the UID and GID for remote root users, edit the /etc/exports file and add the following for each export: anonuid=value greater than UID_MAX from /etc/login.defs anongid=value greater than GID_MAX from /etc/login.defs Note that a value of "-1" is technically acceptable as this will randomize the anonuid and anongid values on a Red Hat Enterprise Linux based NFS server. While acceptable from a security perspective, a value of -1 may cause interoperability issues, particularly with Red Hat Enterprise Linux 7 client systems. Alternatively, functionally equivalent values of 60001, 65534, 65535 may be used. Specifying the anonymous UID and GID ensures that the remote root user is mapped to a local account which has no permissions on the system. Configure NFS Servers The steps in this section are appropriate for systems which operate as NFS servers. Ensure All-Squashing Disabled On All Exports The all_squash maps all uids and gids to an anonymous user. This should be disabled by removing any instances of the all_squash option from the file /etc/exports. The all_squash option maps all client requests to a single anonymous uid/gid on the NFS server, negating the ability to track file access by user ID. Ensure Insecure File Locking is Not Allowed By default the NFS server requires secure file-lock requests, which require credentials from the client in order to lock a file. Most NFS clients send credentials with file lock requests, however, there are a few clients that do not send credentials when requesting a file-lock, allowing the client to only be able to lock world-readable files. To get around this, the insecure_locks option can be used so these clients can access the desired export. This poses a security risk by potentially allowing the client access to data for which it does not have authorization. Remove any instances of the insecure_locks option from the file /etc/exports. Allowing insecure file locking could allow for sensitive data to be viewed or edited by an unauthorized user. Restrict NFS Clients to Privileged Ports By default, the server NFS implementation requires that all client requests be made from ports less than 1024. If your organization has control over systems connected to its network, and if NFS requests are prohibited at the border firewall, this offers some protection against malicious requests from unprivileged users. Therefore, the default should not be changed. To ensure that the default has not been changed, ensure no line in /etc/exports contains the option insecure. 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-7(a) CM-7(b) CM-6(a) PR.AC-4 PR.AC-6 PR.PT-3 Allowing client requests to be made from ports higher than 1024 could allow a unprivileged user to initiate an NFS connection. If the unprivileged user account has been compromised, an attacker could gain access to data on the NFS server. Use Root-Squashing on All Exports If a filesystem is exported using root squashing, requests from root on the client are considered to be unprivileged (mapped to a user such as nobody). This provides some mild protection against remote abuse of an NFS server. Root squashing is enabled by default, and should not be disabled. Ensure that no line in /etc/exports contains the option no_root_squash. If the NFS server allows root access to local file systems from remote hosts, this access could be used to compromise the system. Network Time Protocol The Network Time Protocol is used to manage the system clock over a network. Computer clocks are not very accurate, so time will drift unpredictably on unmanaged systems. Central time protocols can be used both to ensure that time is consistent among a network of systems, and that their time is consistent with the outside world. If every system on a network reliably reports the same time, then it is much easier to correlate log messages in case of an attack. In addition, a number of cryptographic protocols (such as Kerberos) use timestamps to prevent certain types of attacks. If your network does not have synchronized time, these protocols may be unreliable or even unusable. Depending on the specifics of the network, global time accuracy may be just as important as local synchronization, or not very important at all. If your network is connected to the Internet, using a public timeserver (or one provided by your enterprise) provides globally accurate timestamps which may be essential in investigating or responding to an attack which originated outside of your network. A typical network setup involves a small number of internal systems operating as NTP servers, and the remainder obtaining time information from those internal servers. There is a choice between the daemons ntpd and chronyd, which are available from the repositories in the ntp and chrony packages respectively. The default chronyd daemon can work well when external time references are only intermittently accesible, can perform well even when the network is congested for longer periods of time, can usually synchronize the clock faster and with better time accuracy, and quickly adapts to sudden changes in the rate of the clock, for example, due to changes in the temperature of the crystal oscillator. Chronyd should be considered for all systems which are frequently suspended or otherwise intermittently disconnected and reconnected to a network. Mobile and virtual systems for example. The ntpd NTP daemon fully supports NTP protocol version 4 (RFC 5905), including broadcast, multicast, manycast clients and servers, and the orphan mode. It also supports extra authentication schemes based on public-key cryptography (RFC 5906). The NTP daemon (ntpd) should be considered for systems which are normally kept permanently on. Systems which are required to use broadcast or multicast IP, or to perform authentication of packets with the Autokey protocol, should consider using ntpd. Refer to https://en.wikipedia.org/wiki/Network_Time_Protocol for more detailed comparison of features of chronyd and ntpd daemon features respectively, and for further guidance how to choose between the two NTP daemons. The upstream manual pages at https://chrony-project.org/documentation.html for chronyd and http://www.ntp.org for ntpd provide additional information on the capabilities and configuration of each of the NTP daemons. Vendor Approved Time Servers The list of vendor-approved time servers 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org 0.us.pool.ntp.mil 0.fedora.pool.ntp.org,1.fedora.pool.ntp.org,2.fedora.pool.ntp.org,3.fedora.pool.ntp.org 0.rhel.pool.ntp.org,1.rhel.pool.ntp.org,2.rhel.pool.ntp.org,3.rhel.pool.ntp.org 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org 0.suse.pool.ntp.org,1.suse.pool.ntp.org,2.suse.pool.ntp.org,3.suse.pool.ntp.org 0.ntp.cloud.aliyuncs.com,1.ntp.aliyun.com,2.ntp1.aliyun.com,3.ntp1.cloud.aliyuncs.com 0.rhel.pool.ntp.org,1.rhel.pool.ntp.org,2.rhel.pool.ntp.org,3.rhel.pool.ntp.org 0.ubuntu.pool.ntp.org,1.ubuntu.pool.ntp.org,2.ubuntu.pool.ntp.org,3.ubuntu.pool.ntp.org 0.almalinux.pool.ntp.org,1.almalinux.pool.ntp.org,2.almalinux.pool.ntp.org,3.almalinux.pool.ntp.org 0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org Maximum NTP or Chrony Poll The maximum NTP or Chrony poll interval number in seconds specified as a power of two. 17 16 10 10 The Chrony package is installed System time should be synchronized between all systems in an environment. This is typically done by establishing an authoritative time server or set of servers and having all systems synchronize their clocks to them. The chrony package can be installed with the following command: $ sudo dnf install chrony 0988 1405 FMT_SMF_EXT.1 Req-10.4 SRG-OS-000355-GPOS-00143 R71 10.6.1 10.6 Time synchronization is important to support time sensitive security mechanisms like Kerberos and also ensures log files have consistent time records across the enterprise, which aids in forensic investigations. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "chrony" ; then dnf install -y "chrony" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-10.4 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_chrony_installed - name: Ensure chrony is installed ansible.builtin.package: name: chrony state: present when: '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-10.4 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_chrony_installed include install_chrony class install_chrony { package { 'chrony': ensure => 'installed', } } package --add=chrony [[packages]] name = "chrony" version = "*" package install chrony dnf install chrony Install the ntp service The ntpd service should be installed. 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) PR.PT-1 Req-10.4 Time synchronization (using NTP) is required by almost all network and administrative tasks (syslog, cryptographic based services (authentication, etc.), etc.). Ntpd is regulary maintained and updated, supporting security features such as RFC 5906. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "ntp" ; then dnf install -y "ntp" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4 - enable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_ntp_installed - name: Ensure ntp is installed ansible.builtin.package: name: ntp state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4 - enable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_ntp_installed include install_ntp class install_ntp { package { 'ntp': ensure => 'installed', } } package --add=ntp [[packages]] name = "ntp" version = "*" package install ntp dnf install ntp The Chronyd service is enabled chrony is a daemon which implements the Network Time Protocol (NTP) is designed to synchronize system clocks across a variety of systems and use a source that is highly accurate. More information on chrony can be found at https://chrony-project.org/. Chrony can be configured to be a client and/or a server. To enable Chronyd service, you can run: # systemctl enable chronyd.service This recommendation only applies if chrony is in use on the system. 0988 1405 SRG-OS-000355-GPOS-00143 If chrony is in use on the system proper configuration is vital to ensuring time synchronization is working properly. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q chrony; }; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'chronyd.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'chronyd.service' fi "$SYSTEMCTL_EXEC" enable 'chronyd.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_chronyd_enabled - name: The Chronyd service is enabled - Enable service chronyd block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: The Chronyd service is enabled - Enable Service chronyd ansible.builtin.systemd: name: chronyd enabled: true state: started masked: false when: - '"chrony" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_chronyd_enabled - special_service_block when: - '"kernel" in ansible_facts.packages' - '"chrony" in ansible_facts.packages' include enable_chronyd class enable_chronyd { service {'chronyd': enable => true, ensure => 'running', } } [customizations.services] enabled = ["chronyd"] service enable chronyd Enable the NTP Daemon Run the following command to determine the current status of the chronyd service: $ sudo systemctl is-active chronyd If the service is running, it should return the following: active Note: The chronyd daemon is enabled by default. Run the following command to determine the current status of the ntpd service: $ sudo systemctl is-active ntpd If the service is running, it should return the following: active Note: The ntpd daemon is not enabled by default. Though as mentioned in the previous sections in certain environments the ntpd daemon might be preferred to be used rather than the chronyd one. Refer to: for guidance which NTP daemon to choose depending on the environment used. 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 3.3.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 0988 1405 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) AU-8(1)(a) AU-12(1) PR.PT-1 Req-10.4.1 SRG-APP-000116-CTR-000235 R71 10.6.1 10.6 Enabling some of chronyd or ntpd services ensures that the NTP daemon will be running and that the system will synchronize its time to any servers specified. This is important whether the system is configured to be a client (and synchronize only its own clock) or it is also acting as an NTP server to other systems. Synchronizing time is essential for authentication services such as Kerberos, but it is also important for maintaining accurate logs and auditing possible security breaches. The chronyd and ntpd NTP daemons offer all of the functionality of ntpdate, which is now deprecated. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if rpm --quiet -q "chrony" ; then if ! /usr/sbin/pidof ntpd ; then /usr/bin/systemctl enable "chronyd" if [[ $(/usr/bin/systemctl is-system-running) != "offline" ]]; then /usr/bin/systemctl start "chronyd" fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. if /usr/bin/systemctl --failed | grep -q "chronyd"; then /usr/bin/systemctl reset-failed "chronyd" fi fi elif rpm --quiet -q "ntp" ; then /usr/bin/systemctl enable "ntpd" if [[ $(/usr/bin/systemctl is-system-running) != "offline" ]]; then /usr/bin/systemctl start "ntpd" fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. if /usr/bin/systemctl --failed | grep -q "ntpd"; then /usr/bin/systemctl reset-failed "ntpd" fi else if ! rpm -q --quiet "chrony" ; then dnf install -y "chrony" fi /usr/bin/systemctl enable "chronyd" if [[ $(/usr/bin/systemctl is-system-running) != "offline" ]]; then /usr/bin/systemctl start "chronyd" fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. if /usr/bin/systemctl --failed | grep -q "chronyd"; then /usr/bin/systemctl reset-failed "chronyd" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.3.7 - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.1 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_chronyd_or_ntpd_enabled - name: Gather the package facts ansible.builtin.package_facts: manager: auto when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.3.7 - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.1 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_chronyd_or_ntpd_enabled - name: Start ntpd service if ntp installed ansible.builtin.systemd_service: name: ntpd enabled: 'yes' state: started masked: 'no' when: - '"kernel" in ansible_facts.packages' - '''ntp'' in ansible_facts.packages' tags: - NIST-800-171-3.3.7 - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.1 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_chronyd_or_ntpd_enabled - name: Start chronyd service if chrony or chronyd installed ansible.builtin.systemd_service: name: chronyd enabled: 'yes' state: started masked: 'no' when: - '"kernel" in ansible_facts.packages' - ('chrony' in ansible_facts.packages) or ('chronyd' in ansible_facts.packages) tags: - NIST-800-171-3.3.7 - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.1 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_chronyd_or_ntpd_enabled Enable the NTP Daemon The ntpd service can be enabled with the following command: $ sudo systemctl enable ntpd.service 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) AU-8(1)(a) PR.PT-1 Req-10.4 10.6.1 10.6 Enabling the ntpd service ensures that the ntpd service will be running and that the system will synchronize its time to any servers specified. This is important whether the system is configured to be a client (and synchronize only its own clock) or it is also acting as an NTP server to other systems. Synchronizing time is essential for authentication services such as Kerberos, but it is also important for maintaining accurate logs and auditing possible security breaches. The NTP daemon offers all of the functionality of ntpdate, which is now deprecated. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q ntp; }; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'ntpd.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'ntpd.service' fi "$SYSTEMCTL_EXEC" enable 'ntpd.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_ntpd_enabled - name: Enable the NTP Daemon - Enable service ntpd block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable the NTP Daemon - Enable Service ntpd ansible.builtin.systemd: name: ntpd enabled: true state: started masked: false when: - '"ntp" in ansible_facts.packages' tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_ntpd_enabled - special_service_block when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' include enable_ntpd class enable_ntpd { service {'ntpd': enable => true, ensure => 'running', } } [customizations.services] enabled = ["ntpd"] service enable ntpd A remote time server for Chrony is configured Chrony is a daemon which implements the Network Time Protocol (NTP). It is designed to synchronize system clocks across a variety of systems and use a source that is highly accurate. More information on chrony can be found at https://chrony-project.org/. Chrony can be configured to be a client and/or a server. Add or edit server or pool lines to /etc/chrony.conf as appropriate: server <remote-server> Multiple servers may be configured. 0988 1405 CM-6(a) AU-8(1)(a) Req-10.4.3 SRG-OS-000355-GPOS-00143 R71 2.3.2 10.6.2 10.6 If chrony is in use on the system proper configuration is vital to ensuring time synchronization is working properly. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q chrony; }; then var_multiple_time_servers='' config_file="/etc/chrony.conf" if ! grep -q '^[[:space:]]*\(server\|pool\)[[:space:]]\+[[:graph:]]\+' "$config_file" ; then if ! grep -q '#[[:space:]]*server' "$config_file" ; then for server in $(echo "$var_multiple_time_servers" | tr ',' '\n') ; do printf '\nserver %s' "$server" >> "$config_file" done else sed -i 's/#[ \t]*server/server/g' "$config_file" fi if [[ -s "$config_file" ]] && [[ -n "$(tail -c 1 -- "$config_file" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$config_file" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.2 - chronyd_specify_remote_server - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_multiple_time_servers # promote to variable set_fact: var_multiple_time_servers: !!str tags: - always - name: Detect if chrony is already configured with pools or servers ansible.builtin.find: path: /etc patterns: chrony.conf contains: ^[\s]*(?:server|pool)[\s]+[\w]+ register: chrony_servers when: - '"kernel" in ansible_facts.packages' - '"chrony" in ansible_facts.packages' tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.2 - chronyd_specify_remote_server - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure remote time servers ansible.builtin.lineinfile: path: /etc/chrony.conf line: server {{ item }} state: present create: true loop: '{{ var_multiple_time_servers.split(",") }}' when: - '"kernel" in ansible_facts.packages' - '"chrony" in ansible_facts.packages' - chrony_servers.matched == 0 tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.2 - chronyd_specify_remote_server - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Disable chrony daemon from acting as server The port option in /etc/chrony.conf can be set to 0 to make chrony daemon to never open any listening port for server operation and to operate strictly in a client-only mode. AU-8(1) AU-12(1) FMT_SMF_EXT.1 SRG-OS-000096-GPOS-00050 SRG-OS-000095-GPOS-00049 In order to prevent unauthorized connection of devices, unauthorized transfer of information, or unauthorized tunneling (i.e., embedding of data types within data types), organizations must disable or restrict unused or unnecessary physical and logical ports/protocols on information systems. Operating systems are capable of providing a wide variety of functions and services. Some of the functions and services provided by default may not be necessary to support essential organizational operations. Additionally, it is sometimes convenient to provide multiple services from a single component (e.g., VPN and IPS); however, doing so increases risk over limiting the services provided by any one component. To support the requirements and principles of least functionality, the operating system must support the organizational requirements, providing only essential capabilities and limiting the use of ports, protocols, and/or services to only those required, authorized, and approved to conduct official business or to address authorized quality of life issues. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q chrony; }; then # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^port") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^port\\>" "/etc/chrony.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^port\\>.*/$escaped_formatted_output/gi" "/etc/chrony.conf" else if [[ -s "/etc/chrony.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/chrony.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/chrony.conf" fi printf '%s\n' "$formatted_output" >> "/etc/chrony.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1) - chronyd_client_only - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Disable chrony daemon from acting as server block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/chrony.conf create: true regexp: (?i)^\s*port\s+ state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/chrony.conf ansible.builtin.lineinfile: path: /etc/chrony.conf create: true regexp: (?i)^\s*port\s+ state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/chrony.conf ansible.builtin.lineinfile: path: /etc/chrony.conf create: true regexp: (?i)^\s*port\s+ line: port 0 state: present when: - '"kernel" in ansible_facts.packages' - '"chrony" in ansible_facts.packages' tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1) - chronyd_client_only - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%20Allow%20for%20extra%20configuration%20files.%20This%20is%20useful%0A%23%20for%20admins%20specifying%20their%20own%20NTP%20servers%0Ainclude%20/etc/chrony.d/%2A.conf%0A%0A%23%20Set%20chronyd%20as%20client-only.%0Aport%200%0A%0A%23%20Disable%20chronyc%20from%20the%20network%0Acmdport%200%0A%0A%23%20Record%20the%20rate%20at%20which%20the%20system%20clock%20gains/losses%20time.%0Adriftfile%20/var/lib/chrony/drift%0A%0A%23%20Allow%20the%20system%20clock%20to%20be%20stepped%20in%20the%20first%20three%20updates%0A%23%20if%20its%20offset%20is%20larger%20than%201%20second.%0Amakestep%201.0%203%0A%0A%23%20Enable%20kernel%20synchronization%20of%20the%20real-time%20clock%20%28RTC%29.%0Artcsync%0A%0A%23%20Enable%20hardware%20timestamping%20on%20all%20interfaces%20that%20support%20it.%0A%23hwtimestamp%20%2A%0A%0A%23%20Increase%20the%20minimum%20number%20of%20selectable%20sources%20required%20to%20adjust%0A%23%20the%20system%20clock.%0A%23minsources%202%0A%0A%23%20Allow%20NTP%20client%20access%20from%20local%20network.%0A%23allow%20192.168.0.0/16%0A%0A%23%20Serve%20time%20even%20if%20not%20synchronized%20to%20a%20time%20source.%0A%23local%20stratum%2010%0A%0A%23%20Require%20authentication%20%28nts%20or%20key%20option%29%20for%20all%20NTP%20sources.%0A%23authselectmode%20require%0A%0A%23%20Specify%20file%20containing%20keys%20for%20NTP%20authentication.%0Akeyfile%20/etc/chrony.keys%0A%0A%23%20Insert/delete%20leap%20seconds%20by%20slewing%20instead%20of%20stepping.%0A%23leapsecmode%20slew%0A%0A%23%20Get%20TAI-UTC%20offset%20and%20leap%20seconds%20from%20the%20system%20tz%20database.%0Aleapsectz%20right/UTC%0A%0A%23%20Specify%20directory%20for%20log%20files.%0Alogdir%20/var/log/chrony%0A%0A%23%20Select%20which%20information%20is%20logged.%0A%23log%20measurements%20statistics%20tracking }} mode: 420 overwrite: true path: /etc/chrony.conf - contents: source: data:, mode: 420 overwrite: true path: /etc/chrony.d/.mco-keep - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20ntp%20server%0A%23%20%7B%7B.var_multiple_time_servers%7D%7D%20we%20have%20to%20put%20variable%20array%20name%20here%20for%20mutilines%20remediation%0A%7B%7B%24var_time_service_set_maxpoll%3A%3D.var_time_service_set_maxpoll%7D%7D%0A%7B%7Brange%20%24element%3A%3D.var_multiple_time_servers%7CtoArrayByComma%7D%7Dserver%20%7B%7B%24element%7D%7D%20minpoll%204%20maxpoll%20%7B%7B%24var_time_service_set_maxpoll%7D%7D%0A%7B%7Bend%7D%7D }} mode: 420 overwrite: true path: /etc/chrony.d/ntp-server.conf Disable network management of chrony daemon The cmdport option in /etc/chrony.conf can be set to 0 to stop chrony daemon from listening on the UDP port 323 for management connections made by chronyc. CM-7(1) SRG-OS-000096-GPOS-00050 SRG-OS-000095-GPOS-00049 Minimizing the exposure of the server functionality of the chrony daemon diminishes the attack surface. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q chrony; }; then # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^cmdport") # shellcheck disable=SC2059 printf -v formatted_output "%s %s" "$stripped_key" "0" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^cmdport\\>" "/etc/chrony.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^cmdport\\>.*/$escaped_formatted_output/gi" "/etc/chrony.conf" else if [[ -s "/etc/chrony.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/chrony.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/chrony.conf" fi printf '%s\n' "$formatted_output" >> "/etc/chrony.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-7(1) - chronyd_no_chronyc_network - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Disable network management of chrony daemon block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/chrony.conf create: true regexp: (?i)^\s*cmdport\s+ state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/chrony.conf ansible.builtin.lineinfile: path: /etc/chrony.conf create: true regexp: (?i)^\s*cmdport\s+ state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/chrony.conf ansible.builtin.lineinfile: path: /etc/chrony.conf create: true regexp: (?i)^\s*cmdport\s+ line: cmdport 0 state: present when: - '"kernel" in ansible_facts.packages' - '"chrony" in ansible_facts.packages' tags: - NIST-800-53-CM-7(1) - chronyd_no_chronyc_network - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%20Allow%20for%20extra%20configuration%20files.%20This%20is%20useful%0A%23%20for%20admins%20specifying%20their%20own%20NTP%20servers%0Ainclude%20/etc/chrony.d/%2A.conf%0A%0A%23%20Set%20chronyd%20as%20client-only.%0Aport%200%0A%0A%23%20Disable%20chronyc%20from%20the%20network%0Acmdport%200%0A%0A%23%20Record%20the%20rate%20at%20which%20the%20system%20clock%20gains/losses%20time.%0Adriftfile%20/var/lib/chrony/drift%0A%0A%23%20Allow%20the%20system%20clock%20to%20be%20stepped%20in%20the%20first%20three%20updates%0A%23%20if%20its%20offset%20is%20larger%20than%201%20second.%0Amakestep%201.0%203%0A%0A%23%20Enable%20kernel%20synchronization%20of%20the%20real-time%20clock%20%28RTC%29.%0Artcsync%0A%0A%23%20Enable%20hardware%20timestamping%20on%20all%20interfaces%20that%20support%20it.%0A%23hwtimestamp%20%2A%0A%0A%23%20Increase%20the%20minimum%20number%20of%20selectable%20sources%20required%20to%20adjust%0A%23%20the%20system%20clock.%0A%23minsources%202%0A%0A%23%20Allow%20NTP%20client%20access%20from%20local%20network.%0A%23allow%20192.168.0.0/16%0A%0A%23%20Serve%20time%20even%20if%20not%20synchronized%20to%20a%20time%20source.%0A%23local%20stratum%2010%0A%0A%23%20Require%20authentication%20%28nts%20or%20key%20option%29%20for%20all%20NTP%20sources.%0A%23authselectmode%20require%0A%0A%23%20Specify%20file%20containing%20keys%20for%20NTP%20authentication.%0Akeyfile%20/etc/chrony.keys%0A%0A%23%20Insert/delete%20leap%20seconds%20by%20slewing%20instead%20of%20stepping.%0A%23leapsecmode%20slew%0A%0A%23%20Get%20TAI-UTC%20offset%20and%20leap%20seconds%20from%20the%20system%20tz%20database.%0Aleapsectz%20right/UTC%0A%0A%23%20Specify%20directory%20for%20log%20files.%0Alogdir%20/var/log/chrony%0A%0A%23%20Select%20which%20information%20is%20logged.%0A%23log%20measurements%20statistics%20tracking }} mode: 420 overwrite: true path: /etc/chrony.conf - contents: source: data:, mode: 420 overwrite: true path: /etc/chrony.d/.mco-keep - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20ntp%20server%0A%23%20%7B%7B.var_multiple_time_servers%7D%7D%20we%20have%20to%20put%20variable%20array%20name%20here%20for%20mutilines%20remediation%0A%7B%7B%24var_time_service_set_maxpoll%3A%3D.var_time_service_set_maxpoll%7D%7D%0A%7B%7Brange%20%24element%3A%3D.var_multiple_time_servers%7CtoArrayByComma%7D%7Dserver%20%7B%7B%24element%7D%7D%20minpoll%204%20maxpoll%20%7B%7B%24var_time_service_set_maxpoll%7D%7D%0A%7B%7Bend%7D%7D }} mode: 420 overwrite: true path: /etc/chrony.d/ntp-server.conf Configure Time Service Maxpoll Interval The maxpoll should be configured to in /etc/ntp.conf or /etc/chrony.conf (or /etc/chrony.d/) to continuously poll time servers. To configure maxpoll in /etc/ntp.conf or /etc/chrony.conf (or /etc/chrony.d/) add the following after each server, pool or peer entry: maxpoll to server directives. If using chrony, any pool directives should be configured too. 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) AU-8(1)(b) AU-12(1) PR.PT-1 SRG-OS-000355-GPOS-00143 SRG-OS-000356-GPOS-00144 SRG-OS-000359-GPOS-00146 Inaccurate time stamps make it more difficult to correlate events and can lead to an inaccurate analysis. Determining the correct time a particular event occurred on a system is critical when conducting forensic analysis and investigating system events. Sources outside the configured acceptable allowance (drift) may be inaccurate. Synchronizing internal information system clocks provides uniformity of time stamps for information systems with multiple system clocks and systems connected over a network. Organizations should consider endpoints that may not have regular access to the authoritative time server (e.g., mobile, teleworking, and tactical endpoints). # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { ( rpm --quiet -q chrony || rpm --quiet -q ntp ); }; then var_time_service_set_maxpoll='' pof="/usr/sbin/pidof" CONFIG_FILES="/etc/ntp.conf" $pof ntpd || { CHRONY_D_PATH=/etc/chrony.d/ mapfile -t CONFIG_FILES < <(find ${CHRONY_D_PATH}.* -type f -name '*.conf') CONFIG_FILES+=(/etc/chrony.conf) } # get list of ntp files for config_file in "${CONFIG_FILES[@]}" ; do # Set maxpoll values to var_time_service_set_maxpoll sed -i "s/^\(\(server\|pool\|peer\).*maxpoll\) [0-9,-][0-9]*\(.*\)$/\1 $var_time_service_set_maxpoll \3/" "$config_file" done for config_file in "${CONFIG_FILES[@]}" ; do # Add maxpoll to server, pool or peer entries without maxpoll grep "^\(server\|pool\|peer\)" "$config_file" | grep -v maxpoll | while read -r line ; do sed -i "s/$line/& maxpoll $var_time_service_set_maxpoll/" "$config_file" done done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_time_service_set_maxpoll # promote to variable set_fact: var_time_service_set_maxpoll: !!str tags: - always - name: Configure Time Service Maxpoll Interval - Check That /etc/ntp.conf Exist ansible.builtin.stat: path: /etc/ntp.conf register: ntp_conf_exist_result when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Update the maxpoll Values in /etc/ntp.conf ansible.builtin.replace: path: /etc/ntp.conf regexp: ^(server.*maxpoll)[ ]+[0-9]+(.*)$ replace: \1 {{ var_time_service_set_maxpoll }}\2 when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - ntp_conf_exist_result.stat.exists tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Set the maxpoll Values in /etc/ntp.conf ansible.builtin.replace: path: /etc/ntp.conf regexp: (^server\s+((?!maxpoll).)*)$ replace: \1 maxpoll {{ var_time_service_set_maxpoll }}\n when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - ntp_conf_exist_result.stat.exists tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Check That /etc/chrony.conf Exist ansible.builtin.stat: path: /etc/chrony.conf register: chrony_conf_exist_result when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Update the maxpoll Values in /etc/chrony.conf ansible.builtin.replace: path: /etc/chrony.conf regexp: ^((?:server|pool|peer).*maxpoll)[ ]+[0-9]+(.*)$ replace: \1 {{ var_time_service_set_maxpoll }}\2 when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - chrony_conf_exist_result.stat.exists tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Set the maxpoll Values in /etc/chrony.conf ansible.builtin.replace: path: /etc/chrony.conf regexp: (^(?:server|pool|peer)\s+((?!maxpoll).)*)$ replace: \1 maxpoll {{ var_time_service_set_maxpoll }}\n when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - chrony_conf_exist_result.stat.exists tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Get Conf Files from /etc/chrony.d/ ansible.builtin.find: path: /etc/chrony.d/ patterns: '*.conf' file_type: file register: chrony_d_conf_files when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Update the maxpoll Values in /etc/chrony.d/ ansible.builtin.replace: path: '{{ item.path }}' regexp: ^((?:server|pool|peer).*maxpoll)[ ]+[0-9,-]+(.*)$ replace: \1 {{ var_time_service_set_maxpoll }}\2 loop: '{{ chrony_d_conf_files.files }}' when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - chrony_d_conf_files.matched tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Configure Time Service Maxpoll Interval - Set the maxpoll Values in /etc/chrony.d/ ansible.builtin.replace: path: '{{ item.path }}' regexp: (^(?:server|pool|peer)\s+((?!maxpoll).)*)$ replace: \1 maxpoll {{ var_time_service_set_maxpoll }}\n loop: '{{ chrony_d_conf_files.files }}' when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - chrony_d_conf_files.matched tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(b) - NIST-800-53-CM-6(a) - chronyd_or_ntpd_set_maxpoll - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%20Allow%20for%20extra%20configuration%20files.%20This%20is%20useful%0A%23%20for%20admins%20specifying%20their%20own%20NTP%20servers%0Ainclude%20/etc/chrony.d/%2A.conf%0A%0A%23%20Set%20chronyd%20as%20client-only.%0Aport%200%0A%0A%23%20Disable%20chronyc%20from%20the%20network%0Acmdport%200%0A%0A%23%20Record%20the%20rate%20at%20which%20the%20system%20clock%20gains/losses%20time.%0Adriftfile%20/var/lib/chrony/drift%0A%0A%23%20Allow%20the%20system%20clock%20to%20be%20stepped%20in%20the%20first%20three%20updates%0A%23%20if%20its%20offset%20is%20larger%20than%201%20second.%0Amakestep%201.0%203%0A%0A%23%20Enable%20kernel%20synchronization%20of%20the%20real-time%20clock%20%28RTC%29.%0Artcsync%0A%0A%23%20Enable%20hardware%20timestamping%20on%20all%20interfaces%20that%20support%20it.%0A%23hwtimestamp%20%2A%0A%0A%23%20Increase%20the%20minimum%20number%20of%20selectable%20sources%20required%20to%20adjust%0A%23%20the%20system%20clock.%0A%23minsources%202%0A%0A%23%20Allow%20NTP%20client%20access%20from%20local%20network.%0A%23allow%20192.168.0.0/16%0A%0A%23%20Serve%20time%20even%20if%20not%20synchronized%20to%20a%20time%20source.%0A%23local%20stratum%2010%0A%0A%23%20Require%20authentication%20%28nts%20or%20key%20option%29%20for%20all%20NTP%20sources.%0A%23authselectmode%20require%0A%0A%23%20Specify%20file%20containing%20keys%20for%20NTP%20authentication.%0Akeyfile%20/etc/chrony.keys%0A%0A%23%20Insert/delete%20leap%20seconds%20by%20slewing%20instead%20of%20stepping.%0A%23leapsecmode%20slew%0A%0A%23%20Get%20TAI-UTC%20offset%20and%20leap%20seconds%20from%20the%20system%20tz%20database.%0Aleapsectz%20right/UTC%0A%0A%23%20Specify%20directory%20for%20log%20files.%0Alogdir%20/var/log/chrony%0A%0A%23%20Select%20which%20information%20is%20logged.%0A%23log%20measurements%20statistics%20tracking }} mode: 420 overwrite: true path: /etc/chrony.conf - contents: source: data:, mode: 420 overwrite: true path: /etc/chrony.d/.mco-keep - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20ntp%20server%0A%23%20%7B%7B.var_multiple_time_servers%7D%7D%20we%20have%20to%20put%20variable%20array%20name%20here%20for%20mutilines%20remediation%0A%7B%7B%24var_time_service_set_maxpoll%3A%3D.var_time_service_set_maxpoll%7D%7D%0A%7B%7Brange%20%24element%3A%3D.var_multiple_time_servers%7CtoArrayByComma%7D%7Dserver%20%7B%7B%24element%7D%7D%20minpoll%204%20maxpoll%20%7B%7B%24var_time_service_set_maxpoll%7D%7D%0A%7B%7Bend%7D%7D }} mode: 420 overwrite: true path: /etc/chrony.d/ntp-server.conf Specify Additional Remote NTP Servers Depending on specific functional requirements of a concrete production environment, the Fedora system can be configured to utilize the services of the chronyd NTP daemon (the default), or services of the ntpd NTP daemon. Refer to for more detailed comparison of the features of both of the choices, and for further guidance how to choose between the two NTP daemons. Additional NTP servers can be specified for time synchronization. To do so, perform the following: if the system is configured to use the chronyd as the NTP daemon (the default), edit the file /etc/chrony.conf as follows, if the system is configured to use the ntpd as the NTP daemon, edit the file /etc/ntp.conf as documented below. Add additional lines of the following form, substituting the IP address or hostname of a remote NTP server for ntpserver: server ntpserver 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 0988 1405 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) AU-8(1)(a) AU-8(2) AU-12(1) PR.PT-1 Req-10.4.3 Specifying additional NTP servers increases the availability of accurate time data, in the event that one of the specified servers becomes unavailable. This is typical for a system acting as an NTP server for other systems. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { ( rpm --quiet -q chrony || rpm --quiet -q ntp ); }; then var_multiple_time_servers='' config_file="/etc/ntp.conf" /usr/sbin/pidof ntpd || config_file="/etc/chrony.conf" if ! [ "$(grep -c '^server' "$config_file")" -gt 1 ] ; then if ! grep -q '#[[:space:]]*server' "$config_file" ; then for server in $(echo "$var_multiple_time_servers" | tr ',' '\n') ; do printf '\nserver %s' "$server" >> "$config_file" done else sed -i 's/#[ \t]*server/server/g' "$config_file" fi if [[ -s "$config_file" ]] && [[ -n "$(tail -c 1 -- "$config_file" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$config_file" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-AU-8(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - chronyd_or_ntpd_specify_multiple_servers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_multiple_time_servers # promote to variable set_fact: var_multiple_time_servers: !!str tags: - always - name: Detect if chrony configuration file is present ansible.builtin.find: path: /etc patterns: chrony.conf register: chrony_server_config when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-AU-8(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - chronyd_or_ntpd_specify_multiple_servers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure multiple time servers in chrony config ansible.builtin.lineinfile: path: /etc/chrony.conf line: server {{ item }} state: present create: true loop: '{{ var_multiple_time_servers.split(",") }}' when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - chrony_server_config.matched == 1 tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-AU-8(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - chronyd_or_ntpd_specify_multiple_servers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Detect if NTP configuration file is present ansible.builtin.find: path: /etc patterns: ntp.conf register: ntp_server_config when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-AU-8(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - chronyd_or_ntpd_specify_multiple_servers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Configure multiple time servers in NTP config ansible.builtin.lineinfile: path: /etc/chrony.conf line: pool {{ item }} state: present create: true loop: '{{ var_multiple_time_servers.split(",") }}' when: - '"kernel" in ansible_facts.packages' - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages ) - ntp_server_config.matched == 1 tags: - NIST-800-53-AU-12(1) - NIST-800-53-AU-8(1)(a) - NIST-800-53-AU-8(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.3 - chronyd_or_ntpd_specify_multiple_servers - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%20Allow%20for%20extra%20configuration%20files.%20This%20is%20useful%0A%23%20for%20admins%20specifying%20their%20own%20NTP%20servers%0Ainclude%20/etc/chrony.d/%2A.conf%0A%0A%23%20Set%20chronyd%20as%20client-only.%0Aport%200%0A%0A%23%20Disable%20chronyc%20from%20the%20network%0Acmdport%200%0A%0A%23%20Record%20the%20rate%20at%20which%20the%20system%20clock%20gains/losses%20time.%0Adriftfile%20/var/lib/chrony/drift%0A%0A%23%20Allow%20the%20system%20clock%20to%20be%20stepped%20in%20the%20first%20three%20updates%0A%23%20if%20its%20offset%20is%20larger%20than%201%20second.%0Amakestep%201.0%203%0A%0A%23%20Enable%20kernel%20synchronization%20of%20the%20real-time%20clock%20%28RTC%29.%0Artcsync%0A%0A%23%20Enable%20hardware%20timestamping%20on%20all%20interfaces%20that%20support%20it.%0A%23hwtimestamp%20%2A%0A%0A%23%20Increase%20the%20minimum%20number%20of%20selectable%20sources%20required%20to%20adjust%0A%23%20the%20system%20clock.%0A%23minsources%202%0A%0A%23%20Allow%20NTP%20client%20access%20from%20local%20network.%0A%23allow%20192.168.0.0/16%0A%0A%23%20Serve%20time%20even%20if%20not%20synchronized%20to%20a%20time%20source.%0A%23local%20stratum%2010%0A%0A%23%20Require%20authentication%20%28nts%20or%20key%20option%29%20for%20all%20NTP%20sources.%0A%23authselectmode%20require%0A%0A%23%20Specify%20file%20containing%20keys%20for%20NTP%20authentication.%0Akeyfile%20/etc/chrony.keys%0A%0A%23%20Insert/delete%20leap%20seconds%20by%20slewing%20instead%20of%20stepping.%0A%23leapsecmode%20slew%0A%0A%23%20Get%20TAI-UTC%20offset%20and%20leap%20seconds%20from%20the%20system%20tz%20database.%0Aleapsectz%20right/UTC%0A%0A%23%20Specify%20directory%20for%20log%20files.%0Alogdir%20/var/log/chrony%0A%0A%23%20Select%20which%20information%20is%20logged.%0A%23log%20measurements%20statistics%20tracking }} mode: 420 overwrite: true path: /etc/chrony.conf - contents: source: data:, mode: 420 overwrite: true path: /etc/chrony.d/.mco-keep - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20ntp%20server%0A%23%20%7B%7B.var_multiple_time_servers%7D%7D%20we%20have%20to%20put%20variable%20array%20name%20here%20for%20mutilines%20remediation%0A%7B%7B%24var_time_service_set_maxpoll%3A%3D.var_time_service_set_maxpoll%7D%7D%0A%7B%7Brange%20%24element%3A%3D.var_multiple_time_servers%7CtoArrayByComma%7D%7Dserver%20%7B%7B%24element%7D%7D%20minpoll%204%20maxpoll%20%7B%7B%24var_time_service_set_maxpoll%7D%7D%0A%7B%7Bend%7D%7D }} mode: 420 overwrite: true path: /etc/chrony.d/ntp-server.conf Specify a Remote NTP Server Depending on specific functional requirements of a concrete production environment, the Fedora system can be configured to utilize the services of the chronyd NTP daemon (the default), or services of the ntpd NTP daemon. Refer to for more detailed comparison of the features of both of the choices, and for further guidance how to choose between the two NTP daemons. To specify a remote NTP server for time synchronization, perform the following: if the system is configured to use the chronyd as the NTP daemon (the default), edit the file /etc/chrony.conf as follows, if the system is configured to use the ntpd as the NTP daemon, edit the file /etc/ntp.conf as documented below. Add or correct the following lines, substituting the IP or hostname of a remote NTP server for ntpserver: server ntpserver This instructs the NTP software to contact that remote server to obtain time data. 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 3.3.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) AU-8(1)(a) AU-8(2) AU-12(1) PR.PT-1 Req-10.4.1 Req-10.4.3 SRG-APP-000116-CTR-000235 Synchronizing with an NTP server makes it possible to collate system logs from multiple sources or correlate computer events with real time events. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { ( rpm --quiet -q chrony || rpm --quiet -q ntp ); }; then var_multiple_time_servers='' config_file="/etc/ntp.conf" /usr/sbin/pidof ntpd || config_file="/etc/chrony.conf" if ! grep -q ^server "$config_file" ; then if ! grep -q '#[[:space:]]*server' "$config_file" ; then for server in $(echo "$var_multiple_time_servers" | tr ',' '\n') ; do printf '\nserver %s' "$server" >> "$config_file" done else sed -i 's/#[ \t]*server/server/g' "$config_file" fi if [[ -s "$config_file" ]] && [[ -n "$(tail -c 1 -- "$config_file" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$config_file" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%20Allow%20for%20extra%20configuration%20files.%20This%20is%20useful%0A%23%20for%20admins%20specifying%20their%20own%20NTP%20servers%0Ainclude%20/etc/chrony.d/%2A.conf%0A%0A%23%20Set%20chronyd%20as%20client-only.%0Aport%200%0A%0A%23%20Disable%20chronyc%20from%20the%20network%0Acmdport%200%0A%0A%23%20Record%20the%20rate%20at%20which%20the%20system%20clock%20gains/losses%20time.%0Adriftfile%20/var/lib/chrony/drift%0A%0A%23%20Allow%20the%20system%20clock%20to%20be%20stepped%20in%20the%20first%20three%20updates%0A%23%20if%20its%20offset%20is%20larger%20than%201%20second.%0Amakestep%201.0%203%0A%0A%23%20Enable%20kernel%20synchronization%20of%20the%20real-time%20clock%20%28RTC%29.%0Artcsync%0A%0A%23%20Enable%20hardware%20timestamping%20on%20all%20interfaces%20that%20support%20it.%0A%23hwtimestamp%20%2A%0A%0A%23%20Increase%20the%20minimum%20number%20of%20selectable%20sources%20required%20to%20adjust%0A%23%20the%20system%20clock.%0A%23minsources%202%0A%0A%23%20Allow%20NTP%20client%20access%20from%20local%20network.%0A%23allow%20192.168.0.0/16%0A%0A%23%20Serve%20time%20even%20if%20not%20synchronized%20to%20a%20time%20source.%0A%23local%20stratum%2010%0A%0A%23%20Require%20authentication%20%28nts%20or%20key%20option%29%20for%20all%20NTP%20sources.%0A%23authselectmode%20require%0A%0A%23%20Specify%20file%20containing%20keys%20for%20NTP%20authentication.%0Akeyfile%20/etc/chrony.keys%0A%0A%23%20Insert/delete%20leap%20seconds%20by%20slewing%20instead%20of%20stepping.%0A%23leapsecmode%20slew%0A%0A%23%20Get%20TAI-UTC%20offset%20and%20leap%20seconds%20from%20the%20system%20tz%20database.%0Aleapsectz%20right/UTC%0A%0A%23%20Specify%20directory%20for%20log%20files.%0Alogdir%20/var/log/chrony%0A%0A%23%20Select%20which%20information%20is%20logged.%0A%23log%20measurements%20statistics%20tracking }} mode: 420 overwrite: true path: /etc/chrony.conf - contents: source: data:, mode: 420 overwrite: true path: /etc/chrony.d/.mco-keep - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20ntp%20server%0A%23%20%7B%7B.var_multiple_time_servers%7D%7D%20we%20have%20to%20put%20variable%20array%20name%20here%20for%20mutilines%20remediation%0A%7B%7B%24var_time_service_set_maxpoll%3A%3D.var_time_service_set_maxpoll%7D%7D%0A%7B%7Brange%20%24element%3A%3D.var_multiple_time_servers%7CtoArrayByComma%7D%7Dserver%20%7B%7B%24element%7D%7D%20minpoll%204%20maxpoll%20%7B%7B%24var_time_service_set_maxpoll%7D%7D%0A%7B%7Bend%7D%7D }} mode: 420 overwrite: true path: /etc/chrony.d/ntp-server.conf Ensure that chronyd is running under chrony user account chrony is a daemon which implements the Network Time Protocol (NTP). It is designed to synchronize system clocks across a variety of systems and use a source that is highly accurate. More information on chrony can be found at https://chrony-project.org/. Chrony can be configured to be a client and/or a server. To ensure that chronyd is running under chrony user account, remove any -u ... option from OPTIONS other than -u chrony, as chrony is run under its own user by default. This recommendation only applies if chrony is in use on the system. 2.3.3 10.6.3 10.6 If chrony is in use on the system proper configuration is vital to ensuring time synchronization is working properly. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q chrony; }; then if grep -q 'OPTIONS=.*' /etc/sysconfig/chronyd; then # trying to solve cases where the parameter after OPTIONS #may or may not be enclosed in quotes sed -i -E -e 's/\s*-u\s*\w+\s*/ /' -e 's/^([\s]*OPTIONS=["]?[^"]*)("?)/\1\2/' /etc/sysconfig/chronyd fi if grep -q 'OPTIONS=.*' /etc/sysconfig/chronyd; then # trying to solve cases where the parameter after OPTIONS #may or may not be enclosed in quotes sed -i -E -e 's/\s*-u\s*\w+\s*/ /' -e 's/^([\s]*OPTIONS=["]?[^"]*)("?)/\1 -u chrony\2/' /etc/sysconfig/chronyd else echo 'OPTIONS="-u chrony"' >> /etc/sysconfig/chronyd fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - chronyd_run_as_chrony_user - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Detect if file /etc/sysconfig/chronyd is not empty or missing ansible.builtin.find: path: /etc/sysconfig/ patterns: chronyd contains: ^([\s]*OPTIONS=["]?[^"]*)("?) register: chronyd_file when: - '"kernel" in ansible_facts.packages' - '"chrony" in ansible_facts.packages' tags: - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - chronyd_run_as_chrony_user - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Remove any previous configuration of user used to run chronyd process ansible.builtin.replace: path: /etc/sysconfig/chronyd regexp: \s*-u\s*\w+\s* replace: ' ' when: - '"kernel" in ansible_facts.packages' - '"chrony" in ansible_facts.packages' - chronyd_file is defined and chronyd_file.matched > 0 tags: - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - chronyd_run_as_chrony_user - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Ensure Chrony is only configured with the server directive Check that Chrony only has time sources configured with the server directive. This rule doesn't come with a remediation, the time source needs to be added by the administrator. SRG-OS-000355-GPOS-00143 SRG-OS-000356-GPOS-00144 SRG-OS-000359-GPOS-00146 Depending on the infrastructure being used the pool directive may not be supported. Using the server directive allows for better control of where the system gets time data from. Configure server restrictions for ntpd ntpd is a daemon which implements the Network Time Protocol (NTP). It is designed to synchronize system clocks across a variety of systems and use a source that is highly accurate. More information on NTP can be found at http://www.ntp.org. ntp can be configured to be a client and/or a server. To ensure that ntpd implements correct server restrictions, make sure that the following lines exist in the file /etc/ntpd.conf: restrict -4 default kod nomodify notrap nopeer noquery restrict -6 default kod nomodify notrap nopeer noquery This recommendation only applies if ntp is in use on the system. If ntp is in use on the system proper configuration is vital to ensuring time synchronization is working properly. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q ntp; }; then if [ -e "/etc/ntp.conf" ] ; then LC_ALL=C sed -i "/^\s*restrict \-4\s\+/Id" "/etc/ntp.conf" else touch "/etc/ntp.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ntp.conf" cp "/etc/ntp.conf" "/etc/ntp.conf.bak" # Insert at the end of the file printf '%s\n' "restrict -4 default kod nomodify notrap nopeer noquery" >> "/etc/ntp.conf" # Clean up after ourselves. rm "/etc/ntp.conf.bak" if [ -e "/etc/ntp.conf" ] ; then LC_ALL=C sed -i "/^\s*restrict \-6\s\+/Id" "/etc/ntp.conf" else touch "/etc/ntp.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ntp.conf" cp "/etc/ntp.conf" "/etc/ntp.conf.bak" # Insert at the end of the file printf '%s\n' "restrict -6 default kod nomodify notrap nopeer noquery" >> "/etc/ntp.conf" # Clean up after ourselves. rm "/etc/ntp.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_configure_restrictions - name: Configure ipv4 restrictions for ntpd ansible.builtin.lineinfile: path: /etc/ntp.conf create: true regexp: '' line: restrict -4 default kod nomodify notrap nopeer noquery state: present when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_configure_restrictions - name: Configure ipv6 restrictions for ntpd ansible.builtin.lineinfile: path: /etc/ntp.conf create: true regexp: '' line: restrict -6 default kod nomodify notrap nopeer noquery state: present when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_configure_restrictions Configure ntpd To Run As ntp User ntp is a daemon which implements the Network Time Protocol (NTP). It is designed to synchronize system clocks across a variety of systems and use a source that is highly accurate. More information on NTP can be found at http://www.ntp.org. ntp can be configured to be a client and/or a server. To ensure that ntpd is running as ntp user, Add or edit the OPTIONS variable in /etc/sysconfig/ntpd to include ' -u ntp:ntp ': OPTIONS="-u ntp:ntp" This recommendation only applies if ntp is in use on the system. If ntp is in use on the system proper configuration is vital to ensuring time synchronization is working properly. Running ntpd under dedicated user accounts limits the attack surface for potential attacker exploiting security flaws in the daemon or the protocol. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q ntp; }; then if grep -q 'OPTIONS=.*' /etc/sysconfig/ntpd; then # trying to solve cases where the parameter after OPTIONS #may or may not be enclosed in quotes sed -i -E 's/^([\s]*OPTIONS=["]?[^"]*)("?)/\1 -u ntp:ntp\2/' /etc/sysconfig/ntpd else echo 'OPTIONS="-u ntp:ntp"' >> /etc/sysconfig/ntpd fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_run_as_ntp_user - name: Detect if file is not empty or missing ansible.builtin.find: path: /etc/sysconfig/ patterns: ntpd contains: ^([\s]*OPTIONS=["]?[^"]*)("?) register: ntpd_file when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_run_as_ntp_user - name: Replace existing setting or create a new file, rest is handled by different task ansible.builtin.lineinfile: path: /etc/sysconfig/ntpd regexp: ^([\s]*OPTIONS=["]?[^"]*)("?) line: \1 -u ntp:ntp\2 state: present create: true backrefs: true when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' - ntpd_file.matched > 0 tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_run_as_ntp_user - name: Put line into file, assume file was empty ansible.builtin.lineinfile: path: /etc/sysconfig/ntpd line: OPTIONS="-u ntp:ntp" state: present create: true when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' - ntpd_file.matched == 0 tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_run_as_ntp_user Specify Additional Remote NTP Servers Additional NTP servers can be specified for time synchronization in the file /etc/ntp.conf. To do so, add additional lines of the following form, substituting the IP address or hostname of a remote NTP server for ntpserver: server ntpserver 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) AU-8(1)(a) AU-8(2) PR.PT-1 Req-10.4.3 10.6.2 10.6 Specifying additional NTP servers increases the availability of accurate time data, in the event that one of the specified servers becomes unavailable. This is typical for a system acting as an NTP server for other systems. Specify a Remote NTP Server To specify a remote NTP server for time synchronization, edit the file /etc/ntp.conf. Add or correct the following lines, substituting the IP or hostname of a remote NTP server for ntpserver: server ntpserver This instructs the NTP software to contact that remote server to obtain time data. 1 14 15 16 3 5 6 APO11.04 BAI03.05 DSS05.04 DSS05.07 MEA02.01 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 CM-6(a) AU-8(1)(a) PR.PT-1 Req-10.4.1 Req-10.4.3 10.6.2 10.6 Synchronizing with an NTP server makes it possible to collate system logs from multiple sources or correlate computer events with real time events. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q ntp; }; then var_multiple_time_servers='' config_file="/etc/ntp.conf" if ! grep -q '^[\s]*(?:server|pool)[\s]+[\w]+' "$config_file" ; then if ! grep -q '#[[:space:]]*server' "$config_file" ; then for server in $(echo "$var_multiple_time_servers" | tr ',' '\n') ; do printf '\nserver %s' "$server" >> "$config_file" done else sed -i 's/#[ \t]*server/server/g' "$config_file" fi if [[ -s "$config_file" ]] && [[ -n "$(tail -c 1 -- "$config_file" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$config_file" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.1 - PCI-DSS-Req-10.4.3 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.2 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_specify_remote_server - name: XCCDF Value var_multiple_time_servers # promote to variable set_fact: var_multiple_time_servers: !!str tags: - always - name: Detect if ntp is already configured with pools or servers ansible.builtin.find: path: /etc patterns: ntp.conf contains: ^[\s]*(?:server|pool)[\s]+[\w]+ register: ntp_servers when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.1 - PCI-DSS-Req-10.4.3 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.2 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_specify_remote_server - name: Configure remote time servers ansible.builtin.lineinfile: path: /etc/ntp.conf line: server {{ item }} state: present create: true loop: '{{ var_multiple_time_servers.split(",") }}' when: - '"kernel" in ansible_facts.packages' - '"ntp" in ansible_facts.packages' - ntp_servers.matched == 0 tags: - NIST-800-53-AU-8(1)(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.1 - PCI-DSS-Req-10.4.3 - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.2 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - ntpd_specify_remote_server Obsolete Services This section discusses a number of network-visible services which have historically caused problems for system security, and for which disabling or severely limiting the service has been the best available guidance for some time. As a result of this, many of these services are not installed as part of Fedora by default. Organizations which are running these services should switch to more secure equivalents as soon as possible. If it remains absolutely necessary to run one of these services for legacy reasons, care should be taken to restrict the service as much as possible, for instance by configuring host firewall software such as iptables to restrict access to the vulnerable service to only those remote hosts which have a known need to use it. Uninstall rsync Package The rsyncd service can be used to synchronize files between systems over network links. The rsync package can be removed with the following command: $ sudo dnf remove rsync 2.1.13 The rsyncd service presents a security risk as it uses unencrypted protocols for communication. # CAUTION: This remediation script will remove rsync # from the system, and may remove any packages # that depend on rsync. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "rsync" ; then dnf remove -y --noautoremove "rsync" fi - name: 'Uninstall rsync Package: Ensure rsync is removed' ansible.builtin.package: name: rsync state: absent tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_rsync_removed include remove_rsync class remove_rsync { package { 'rsync': ensure => 'purged', } } package --remove=rsync package remove rsync dnf remove rsync Ensure rsyncd service is disabled The rsyncd service can be disabled with the following command: $ sudo systemctl mask --now rsyncd.service 2.2.4 2.2 The rsyncd service presents a security risk as it uses unencrypted protocols for communication. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rsyncd.service' fi "$SYSTEMCTL_EXEC" disable 'rsyncd.service' "$SYSTEMCTL_EXEC" mask 'rsyncd.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files rsyncd.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'rsyncd.socket' fi "$SYSTEMCTL_EXEC" mask 'rsyncd.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'rsyncd.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_rsyncd_disabled - name: Ensure rsyncd service is disabled - Disable service rsyncd block: - name: Ensure rsyncd service is disabled - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Ensure rsyncd service is disabled - Ensure rsyncd.service is Masked ansible.builtin.systemd: name: rsyncd.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("rsyncd.service", multiline=True) - name: Unit Socket Exists - rsyncd.socket ansible.builtin.command: systemctl -q list-unit-files rsyncd.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Ensure rsyncd service is disabled - Disable Socket rsyncd ansible.builtin.systemd: name: rsyncd.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("rsyncd.socket", multiline=True) tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_rsyncd_disabled - special_service_block when: '"kernel" in ansible_facts.packages' include disable_rsyncd class disable_rsyncd { service {'rsyncd': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: rsyncd.service enabled: false mask: true - name: rsyncd.socket enabled: false mask: true [customizations.services] masked = ["rsyncd"] service disable rsyncd Xinetd The xinetd service acts as a dedicated listener for some network services (mostly, obsolete ones) and can be used to provide access controls and perform some logging. It has been largely obsoleted by other features, and it is not installed by default. The older Inetd service is not even available as part of Fedora. Uninstall xinetd Package The xinetd package can be removed with the following command: $ sudo dnf remove xinetd 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 R62 2.2.4 2.2 Removing the xinetd package decreases the risk of the xinetd service's accidental (or intentional) activation. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # CAUTION: This remediation script will remove xinetd # from the system, and may remove any packages # that depend on xinetd. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "xinetd" ; then dnf remove -y --noautoremove "xinetd" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_xinetd_removed - name: 'Uninstall xinetd Package: Ensure xinetd is removed' ansible.builtin.package: name: xinetd state: absent when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_xinetd_removed include remove_xinetd class remove_xinetd { package { 'xinetd': ensure => 'purged', } } package --remove=xinetd package remove xinetd dnf remove xinetd NIS The Network Information Service (NIS), also known as 'Yellow Pages' (YP), and its successor NIS+ have been made obsolete by Kerberos, LDAP, and other modern centralized authentication services. NIS should not be used because it suffers from security problems inherent in its design, such as inadequate protection of important authentication information. Remove NIS Client The Network Information Service (NIS), formerly known as Yellow Pages, is a client-server directory service protocol used to distribute system configuration files. The NIS client (ypbind) was used to bind a system to an NIS server and receive the distributed configuration files. 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) R62 2.2.4 2.2 The NIS service is inherently an insecure system that has been vulnerable to DOS attacks, buffer overflows and has poor authentication for querying NIS maps. NIS generally has been replaced by such protocols as Lightweight Directory Access Protocol (LDAP). It is recommended that the service be removed. # CAUTION: This remediation script will remove ypbind # from the system, and may remove any packages # that depend on ypbind. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "ypbind" ; then dnf remove -y --noautoremove "ypbind" fi - name: 'Remove NIS Client: Ensure ypbind is removed' ansible.builtin.package: name: ypbind state: absent tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_ypbind_removed - unknown_severity include remove_ypbind class remove_ypbind { package { 'ypbind': ensure => 'purged', } } package --remove=ypbind package remove ypbind dnf remove ypbind Uninstall ypserv Package The ypserv package can be removed with the following command: $ sudo dnf remove ypserv 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) IA-5(1)(c) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 Req-2.2.2 SRG-OS-000095-GPOS-00049 R62 2.2.4 2.2 The NIS service provides an unencrypted authentication service which does not provide for the confidentiality and integrity of user passwords or the remote session. Removing the ypserv package decreases the risk of the accidental (or intentional) activation of NIS or NIS+ services. # CAUTION: This remediation script will remove ypserv # from the system, and may remove any packages # that depend on ypserv. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "ypserv" ; then dnf remove -y --noautoremove "ypserv" fi - name: 'Uninstall ypserv Package: Ensure ypserv is removed' ansible.builtin.package: name: ypserv state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-IA-5(1)(c) - PCI-DSS-Req-2.2.2 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_ypserv_removed include remove_ypserv class remove_ypserv { package { 'ypserv': ensure => 'purged', } } package --remove=ypserv package remove ypserv dnf remove ypserv Rlogin, Rsh, and Rexec The Berkeley r-commands are legacy services which allow cleartext remote access and have an insecure trust model. Uninstall rsh-server Package The rsh-server package can be removed with the following command: $ sudo dnf remove rsh-server 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) IA-5(1)(c) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000095-GPOS-00049 R62 2.2.4 2.2 The rsh-server service provides unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session and has very weak authentication. If a privileged user were to login using this service, the privileged user password could be compromised. The rsh-server package provides several obsolete and insecure network services. Removing it decreases the risk of those services' accidental (or intentional) activation. # CAUTION: This remediation script will remove rsh-server # from the system, and may remove any packages # that depend on rsh-server. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "rsh-server" ; then dnf remove -y --noautoremove "rsh-server" fi - name: 'Uninstall rsh-server Package: Ensure rsh-server is removed' ansible.builtin.package: name: rsh-server state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-IA-5(1)(c) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_rsh-server_removed include remove_rsh-server class remove_rsh-server { package { 'rsh-server': ensure => 'purged', } } package --remove=rsh-server package remove rsh-server dnf remove rsh-server Uninstall rsh Package The rsh package contains the client commands for the rsh services 3.1.13 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) A.8.2.3 A.13.1.1 A.13.2.1 A.13.2.3 A.14.1.2 A.14.1.3 R62 2.2.4 2.2 These legacy clients contain numerous security exposures and have been replaced with the more secure SSH package. Even if the server is removed, it is best to ensure the clients are also removed to prevent users from inadvertently attempting to use these commands and therefore exposing their credentials. Note that removing the rsh package removes the clients for rsh,rcp, and rlogin. # CAUTION: This remediation script will remove rsh # from the system, and may remove any packages # that depend on rsh. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "rsh" ; then dnf remove -y --noautoremove "rsh" fi - name: 'Uninstall rsh Package: Ensure rsh is removed' ansible.builtin.package: name: rsh state: absent tags: - NIST-800-171-3.1.13 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_rsh_removed - unknown_severity include remove_rsh class remove_rsh { package { 'rsh': ensure => 'purged', } } package --remove=rsh package remove rsh dnf remove rsh Remove Rsh Trust Files The files /etc/hosts.equiv and ~/.rhosts (in each user's home directory) list remote hosts and users that are trusted by the local system when using the rshd daemon. To remove these files, run the following command to delete them from any location: $ sudo rm /etc/hosts.equiv $ rm ~/.rhosts 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 This action is only meaningful if .rhosts support is permitted through PAM. Trust files are convenient, but when used in conjunction with the R-services, they can allow unauthenticated access to a system. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - high_severity - low_complexity - low_disruption - no_reboot_needed - no_rsh_trust_files - restrict_strategy - name: Detect .rhosts files in users home directories ansible.builtin.find: paths: - /root - /home recurse: true patterns: .rhosts hidden: true file_type: file check_mode: false register: rhosts_locations when: '"rsh-server" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - high_severity - low_complexity - low_disruption - no_reboot_needed - no_rsh_trust_files - restrict_strategy - name: Remove .rhosts files ansible.builtin.file: path: '{{ item }}' state: absent with_items: '{{ rhosts_locations.files | map(attribute=''path'') | list }}' when: - '"rsh-server" in ansible_facts.packages' - rhosts_locations is success tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - high_severity - low_complexity - low_disruption - no_reboot_needed - no_rsh_trust_files - restrict_strategy - name: Remove /etc/hosts.equiv file ansible.builtin.file: path: /etc/hosts.equiv state: absent when: '"rsh-server" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - high_severity - low_complexity - low_disruption - no_reboot_needed - no_rsh_trust_files - restrict_strategy Chat/Messaging Services The talk software makes it possible for users to send and receive messages across systems through a terminal session. Uninstall talk-server Package The talk-server package can be removed with the following command: $ sudo dnf remove talk-server 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) R62 2.2.4 2.2 The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the talk-server package decreases the risk of the accidental (or intentional) activation of talk services. # CAUTION: This remediation script will remove talk-server # from the system, and may remove any packages # that depend on talk-server. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "talk-server" ; then dnf remove -y --noautoremove "talk-server" fi - name: 'Uninstall talk-server Package: Ensure talk-server is removed' ansible.builtin.package: name: talk-server state: absent tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_talk-server_removed include remove_talk-server class remove_talk-server { package { 'talk-server': ensure => 'purged', } } package --remove=talk-server package remove talk-server dnf remove talk-server Uninstall talk Package The talk package contains the client program for the Internet talk protocol, which allows the user to chat with other users on different systems. Talk is a communication program which copies lines from one terminal to the terminal of another user. The talk package can be removed with the following command: $ sudo dnf remove talk 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) R62 2.2.4 2.2 The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the talk package decreases the risk of the accidental (or intentional) activation of talk client program. # CAUTION: This remediation script will remove talk # from the system, and may remove any packages # that depend on talk. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "talk" ; then dnf remove -y --noautoremove "talk" fi - name: 'Uninstall talk Package: Ensure talk is removed' ansible.builtin.package: name: talk state: absent tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_talk_removed include remove_talk class remove_talk { package { 'talk': ensure => 'purged', } } package --remove=talk package remove talk dnf remove talk Telnet The telnet protocol does not provide confidentiality or integrity for information transmitted on the network. This includes authentication information such as passwords. Organizations which use telnet should be actively working to migrate to a more secure protocol. Uninstall telnet-server Package The telnet-server package can be removed with the following command: $ sudo dnf remove telnet-server 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 Req-2.2.2 SRG-OS-000095-GPOS-00049 R62 2.1.16 2.2.4 2.2 It is detrimental for operating systems to provide, or install by default, functionality exceeding requirements or mission objectives. These unnecessary capabilities are often overlooked and therefore may remain unsecure. They increase the risk to the platform by providing additional attack vectors. The telnet service provides an unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session. If a privileged user were to login using this service, the privileged user password could be compromised. Removing the telnet-server package decreases the risk of the telnet service's accidental (or intentional) activation. # CAUTION: This remediation script will remove telnet-server # from the system, and may remove any packages # that depend on telnet-server. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "telnet-server" ; then dnf remove -y --noautoremove "telnet-server" fi - name: 'Uninstall telnet-server Package: Ensure telnet-server is removed' ansible.builtin.package: name: telnet-server state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-2.2.2 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_telnet-server_removed include remove_telnet-server class remove_telnet-server { package { 'telnet-server': ensure => 'purged', } } package --remove=telnet-server package remove telnet-server dnf remove telnet-server Remove telnet Clients The telnet client allows users to start connections to other systems via the telnet protocol. 3.1.13 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) A.8.2.3 A.13.1.1 A.13.2.1 A.13.2.3 A.14.1.2 A.14.1.3 R62 2.2.4 2.2.4 2.2 The telnet protocol is insecure and unencrypted. The use of an unencrypted transmission medium could allow an unauthorized user to steal credentials. The ssh package provides an encrypted session and stronger security and is included in Fedora. # CAUTION: This remediation script will remove telnet # from the system, and may remove any packages # that depend on telnet. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "telnet" ; then dnf remove -y --noautoremove "telnet" fi - name: 'Remove telnet Clients: Ensure telnet is removed' ansible.builtin.package: name: telnet state: absent tags: - NIST-800-171-3.1.13 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_telnet_removed include remove_telnet class remove_telnet { package { 'telnet': ensure => 'purged', } } package --remove=telnet package remove telnet dnf remove telnet TFTP Server TFTP is a lightweight version of the FTP protocol which has traditionally been used to configure networking equipment. However, TFTP provides little security, and modern versions of networking operating systems frequently support configuration via SSH or other more secure protocols. A TFTP server should be run only if no more secure method of supporting existing equipment can be found. Uninstall tftp-server Package The tftp-server package can be removed with the following command: $ sudo dnf remove tftp-server 11 12 14 15 3 8 9 APO13.01 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS01.04 DSS05.02 DSS05.03 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.2.1 A.6.2.2 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.IP-1 PR.PT-3 PR.PT-4 SRG-OS-000480-GPOS-00227 R62 2.1.17 2.2.4 2.2 Removing the tftp-server package decreases the risk of the accidental (or intentional) activation of tftp services. If TFTP is required for operational support (such as transmission of router configurations), its use must be documented with the Information Systems Securty Manager (ISSM), restricted to only authorized personnel, and have access control rules established. # CAUTION: This remediation script will remove tftp-server # from the system, and may remove any packages # that depend on tftp-server. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "tftp-server" ; then dnf remove -y --noautoremove "tftp-server" fi - name: 'Uninstall tftp-server Package: Ensure tftp-server is removed' ansible.builtin.package: name: tftp-server state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - package_tftp-server_removed include remove_tftp-server class remove_tftp-server { package { 'tftp-server': ensure => 'purged', } } package --remove=tftp-server package remove tftp-server dnf remove tftp-server Remove tftp Daemon Trivial File Transfer Protocol (TFTP) is a simple file transfer protocol, typically used to automatically transfer configuration or boot files between systems. TFTP does not support authentication and can be easily hacked. The package tftp is a client program that allows for connections to a tftp server. SRG-OS-000074-GPOS-00042 R62 2.2.5 2.2.4 2.2 It is recommended that TFTP be removed, unless there is a specific need for TFTP (such as a boot server). In that case, use extreme caution when configuring the services. # CAUTION: This remediation script will remove tftp # from the system, and may remove any packages # that depend on tftp. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "tftp" ; then dnf remove -y --noautoremove "tftp" fi - name: 'Remove tftp Daemon: Ensure tftp is removed' ansible.builtin.package: name: tftp state: absent tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - package_tftp_removed include remove_tftp class remove_tftp { package { 'tftp': ensure => 'purged', } } package --remove=tftp package remove tftp dnf remove tftp Print Support The Common Unix Printing System (CUPS) service provides both local and network printing support. A system running the CUPS service can accept print jobs from other systems, process them, and send them to the appropriate printer. It also provides an interface for remote administration through a web browser. The CUPS service is installed and activated by default. The project homepage and more detailed documentation are available at http://www.cups.org. Disable the CUPS Service The cups service can be disabled with the following command: $ sudo systemctl mask --now cups.service 11 14 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.05 DSS06.06 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.9.1.2 CM-7(a) CM-7(b) CM-6(a) PR.IP-1 PR.PT-3 2.1.11 Turn off unneeded services to reduce attack surface. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'cups.service' fi "$SYSTEMCTL_EXEC" disable 'cups.service' "$SYSTEMCTL_EXEC" mask 'cups.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files cups.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'cups.socket' fi "$SYSTEMCTL_EXEC" mask 'cups.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'cups.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_cups_disabled - unknown_severity - name: Disable the CUPS Service - Disable service cups block: - name: Disable the CUPS Service - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable the CUPS Service - Ensure cups.service is Masked ansible.builtin.systemd: name: cups.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("cups.service", multiline=True) - name: Unit Socket Exists - cups.socket ansible.builtin.command: systemctl -q list-unit-files cups.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable the CUPS Service - Disable Socket cups ansible.builtin.systemd: name: cups.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("cups.socket", multiline=True) tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - no_reboot_needed - service_cups_disabled - special_service_block - unknown_severity when: '"kernel" in ansible_facts.packages' include disable_cups class disable_cups { service {'cups': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: cups.service enabled: false mask: true - name: cups.socket enabled: false mask: true [customizations.services] masked = ["cups"] service disable cups Proxy Server A proxy server is a very desirable target for a potential adversary because much (or all) sensitive data for a given infrastructure may flow through it. Therefore, if one is required, the system acting as a proxy server should be dedicated to that purpose alone and be stored in a physically secure location. The system's default proxy server software is Squid, and provided in an RPM package of the same name. Disable Squid if Possible If Squid was installed and activated, but the system does not need to act as a proxy server, then it should be disabled and removed. Uninstall squid Package The squid package can be removed with the following command: $ sudo dnf remove squid 2.1.18 If there is no need to make the proxy server software available, removing it provides a safeguard against its activation. # CAUTION: This remediation script will remove squid # from the system, and may remove any packages # that depend on squid. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "squid" ; then dnf remove -y --noautoremove "squid" fi - name: 'Uninstall squid Package: Ensure squid is removed' ansible.builtin.package: name: squid state: absent tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_squid_removed - unknown_severity include remove_squid class remove_squid { package { 'squid': ensure => 'purged', } } package --remove=squid package remove squid dnf remove squid Hardware RNG Entropy Gatherer Daemon The rngd feeds random data from hardware device to kernel random device. Enable the Hardware RNG Entropy Gatherer Service The Hardware RNG Entropy Gatherer service should be enabled. The rngd service can be enabled with the following command: $ sudo systemctl enable rngd.service SRG-OS-000480-GPOS-00227 The rngd service feeds random data from hardware device to kernel random device. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'rngd.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'rngd.service' fi "$SYSTEMCTL_EXEC" enable 'rngd.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - service_rngd_enabled - name: Enable the Hardware RNG Entropy Gatherer Service - Enable service rngd block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable the Hardware RNG Entropy Gatherer Service - Enable Service rngd ansible.builtin.systemd: name: rngd enabled: true state: started masked: false when: - '"rng-tools" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - low_severity - no_reboot_needed - service_rngd_enabled - special_service_block when: '"kernel" in ansible_facts.packages' include enable_rngd class enable_rngd { service {'rngd': enable => true, ensure => 'running', } } [customizations.services] enabled = ["rngd"] service enable rngd Samba(SMB) Microsoft Windows File Sharing Server When properly configured, the Samba service allows Linux systems to provide file and print sharing to Microsoft Windows systems. There are two software packages that provide Samba support. The first, samba-client, provides a series of command line tools that enable a client system to access Samba shares. The second, simply labeled samba, provides the Samba service. It is this second package that allows a Linux system to act as an Active Directory server, a domain controller, or as a domain member. Only the samba-client package is installed by default. Disable Samba if Possible Even after the Samba server package has been installed, it will remain disabled. Do not enable this service unless it is absolutely necessary to provide Microsoft Windows file and print sharing functionality. Uninstall Samba Package The samba package can be removed with the following command: $ sudo dnf remove samba 2.1.14 If there is no need to make the Samba software available, removing it provides a safeguard against its activation. # CAUTION: This remediation script will remove samba # from the system, and may remove any packages # that depend on samba. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "samba" ; then dnf remove -y --noautoremove "samba" fi - name: 'Uninstall Samba Package: Ensure samba is removed' ansible.builtin.package: name: samba state: absent tags: - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_samba_removed - unknown_severity include remove_samba class remove_samba { package { 'samba': ensure => 'purged', } } package --remove=samba package remove samba dnf remove samba SNMP Server The Simple Network Management Protocol allows administrators to monitor the state of network devices, including computers. Older versions of SNMP were well-known for weak security, such as plaintext transmission of the community string (used for authentication) and usage of easily-guessable choices for the community string. Disable SNMP Server if Possible The system includes an SNMP daemon that allows for its remote monitoring, though it not installed by default. If it was installed and activated but is not needed, the software should be disabled and removed. Uninstall net-snmp Package The net-snmp package provides the snmpd service. The net-snmp package can be removed with the following command: $ sudo dnf remove net-snmp 2.1.15 2.2.4 2.2 If there is no need to run SNMP server software, removing the package provides a safeguard against its activation. # CAUTION: This remediation script will remove net-snmp # from the system, and may remove any packages # that depend on net-snmp. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "net-snmp" ; then dnf remove -y --noautoremove "net-snmp" fi - name: 'Uninstall net-snmp Package: Ensure net-snmp is removed' ansible.builtin.package: name: net-snmp state: absent tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.4 - disable_strategy - low_complexity - low_disruption - no_reboot_needed - package_net-snmp_removed - unknown_severity include remove_net-snmp class remove_net-snmp { package { 'net-snmp': ensure => 'purged', } } package --remove=net-snmp package remove net-snmp dnf remove net-snmp Configure SNMP Server if Necessary If it is necessary to run the snmpd agent on the system, some best practices should be followed to minimize the security risk from the installation. The multiple security models implemented by SNMP cannot be fully covered here so only the following general configuration advice can be offered: use only SNMP version 3 security models and enable the use of authentication and encryptionwrite access to the MIB (Management Information Base) should be allowed only if necessaryall access to the MIB should be restricted following a principle of least privilegenetwork access should be limited to the maximum extent possible including restricting to expected network addresses both in the configuration files and in the system firewall rulesensure SNMP agents send traps only to, and accept SNMP queries only from, authorized management stationsensure that permissions on the snmpd.conf configuration file (by default, in /etc/snmp) are 640 or more restrictiveensure that any MIB files' permissions are also 640 or more restrictive SNMP read-only community string Specify the SNMP community string used for read-only access. changemero SNMP read-write community string Specify the SNMP community string used for read-write access. changemerw Ensure SNMP Read Write is disabled Edit /etc/snmp/snmpd.conf, remove any rwuser entries. Once the read write users have been removed, restart the SNMP service: $ sudo systemctl restart snmpd Certain SNMP settings can permit users to execute system behaviors from user writes to the community strings. This may permit a compromised account to execute commands on a remote system. # Remediation is applicable only in certain platforms if rpm --quiet -q net-snmp; then if grep -s "rwuser" /etc/snmp/snmpd.conf | grep -qv "^#"; then sed -i "/^\s*#/b;/rwuser/ s/^/#/" /etc/snmp/snmpd.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi Ensure Default SNMP Password Is Not Used Edit /etc/snmp/snmpd.conf, remove or change the default community strings of public and private. This profile configures new read-only community string to and read-write community string to . Once the default community strings have been changed, restart the SNMP service: $ sudo systemctl restart snmpd 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 IA-5(e) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000480-GPOS-00227 Whether active or not, default simple network management protocol (SNMP) community strings must be changed to maintain security. If the service is running with the default authenticators, then anyone can gather data about the system and the network and use the information to potentially compromise the integrity of the system and network(s). # Remediation is applicable only in certain platforms if rpm --quiet -q net-snmp; then var_snmpd_ro_string='' var_snmpd_rw_string='' # remediate read-only community string if grep -q 'public' /etc/snmp/snmpd.conf; then sed -i "s/public/$var_snmpd_ro_string/" /etc/snmp/snmpd.conf fi # remediate read-write community string if grep -q 'private' /etc/snmp/snmpd.conf; then sed -i "s/private/$var_snmpd_rw_string/" /etc/snmp/snmpd.conf fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-IA-5(e) - configure_strategy - high_severity - low_complexity - medium_disruption - no_reboot_needed - snmpd_not_default_password - name: XCCDF Value var_snmpd_ro_string # promote to variable set_fact: var_snmpd_ro_string: !!str tags: - always - name: XCCDF Value var_snmpd_rw_string # promote to variable set_fact: var_snmpd_rw_string: !!str tags: - always - name: Check if file /etc/snmp/snmpd.conf exists ansible.builtin.stat: path: /etc/snmp/snmpd.conf register: snmpd when: '"net-snmp" in ansible_facts.packages' tags: - NIST-800-53-IA-5(e) - configure_strategy - high_severity - low_complexity - medium_disruption - no_reboot_needed - snmpd_not_default_password - name: Replace all instances of SNMP RO strings ansible.builtin.replace: path: /etc/snmp/snmpd.conf regexp: public replace: '{{ var_snmpd_ro_string }}' when: - '"net-snmp" in ansible_facts.packages' - (snmpd.stat.exists is defined and snmpd.stat.exists) tags: - NIST-800-53-IA-5(e) - configure_strategy - high_severity - low_complexity - medium_disruption - no_reboot_needed - snmpd_not_default_password - name: Replace all instances of SNMP RW strings ansible.builtin.replace: path: /etc/snmp/snmpd.conf regexp: private replace: '{{ var_snmpd_rw_string }}' when: - '"net-snmp" in ansible_facts.packages' - (snmpd.stat.exists is defined and snmpd.stat.exists) tags: - NIST-800-53-IA-5(e) - configure_strategy - high_severity - low_complexity - medium_disruption - no_reboot_needed - snmpd_not_default_password Configure SNMP Service to Use Only SNMPv3 or Newer Edit /etc/snmp/snmpd.conf, removing any references to rocommunity, rwcommunity, or com2sec. Upon doing that, restart the SNMP service: $ sudo systemctl restart snmpd 1311 Earlier versions of SNMP are considered insecure, as they potentially allow unauthorized access to detailed system management information. SSH Server The SSH protocol is recommended for remote login and remote file transfer. SSH provides confidentiality and integrity for data exchanged between two systems, as well as server authentication, through the use of public key cryptography. The implementation included with the system is called OpenSSH, and more detailed documentation is available from its website, https://www.openssh.com. Its server program is called sshd and provided by the RPM package openssh-server. SSH Approved ciphers by FIPS Specify the FIPS approved ciphers that are used for data integrity protection by the SSH server. aes256-ctr,aes192-ctr,aes128-ctr aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes128-ctr aes256-gcm@openssh.com,aes256-ctr,aes128-gcm@openssh.com,aes128-ctr aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,3des-cbc,aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se -3des-cbc,aes128-cbc,aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se -3des-cbc,aes128-cbc,aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com aes256-ctr,aes256-gcm@openssh.com,aes192-ctr,aes128-ctr,aes128-gcm@openssh.com aes256-gcm@openssh.com,aes256-ctr,aes128-gcm@openssh.com,aes128-ctr SSH Approved MACs by FIPS Specify the FIPS approved MACs (message authentication code) algorithms that are used for data integrity protection by the SSH server. hmac-sha2-512,hmac-sha2-256 hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512 hmac-sha2-512,hmac-sha2-256,hmac-sha1,hmac-sha1-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 hmac-sha2-512,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-256-etm@openssh.com hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512 SSH session Idle time Specify duration of allowed idle time. 600 7200 840 900 1800 300 3600 300 SSH Max authentication attempts Specify the maximum number of authentication attempts per connection. 10 3 4 5 4 SSH is required to be installed Specify if the Policy requires SSH to be installed. Used by SSH Rules to determine if SSH should be uninstalled or configured. A value of 0 means that the policy doesn't care if OpenSSH server is installed or not. If it is installed, scanner will check for it's configuration, if it's not installed, the check will pass. A value of 1 indicates that OpenSSH server package is not required by the policy; A value of 2 indicates that OpenSSH server package is required by the policy. 0 1 2 SSH Strong KEX by FIPS Specify the FIPS approved KEXs (Key Exchange Algorithms) algorithms that are used for methods in cryptography by which cryptographic keys are exchanged between two parties ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256 ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256 -diffie-hellman-group1-sha1,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1 -diffie-hellman-group1-sha1,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1 curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256 curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256 curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256 sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256 curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256 sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256 SSH Strong MACs by FIPS Specify the FIPS approved MACs (Message Authentication Code) algorithms that are used for data integrity protection by the SSH server. hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-ripemd160 -hmac-md5,hmac-md5-96,hmac-ripemd160,hmac-sha1-96,umac-64@openssh.com,hmac-md5-etm@openssh.com,hmac-md5-96-etm@openssh.com,hmac-ripemd160-etm@openssh.com,hmac-sha1-96-etm@openssh.com,umac-64-etm@openssh.com -hmac-md5,hmac-md5-96,hmac-ripemd160,hmac-sha1-96,umac-64@openssh.com,hmac-md5-etm@openssh.com,hmac-md5-96-etm@openssh.com,hmac-ripemd160-etm@openssh.com,hmac-sha1-96-etm@openssh.com,umac-64-etm@openssh.com hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-ripemd160 hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 hmac-sha2-512,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512 hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512 hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 SSH Max Sessions Count Specify the maximum number of open sessions permitted. 10 4 3 2 1 0 10 SSH Max Keep Alive Count Specify the maximum number of idle message counts before session is terminated. 10 3 5 0 1 0 Install the OpenSSH Server Package The openssh-server package should be installed. The openssh-server package can be installed with the following command: $ sudo dnf install openssh-server 13 14 APO01.06 DSS05.02 DSS05.04 DSS05.07 DSS06.02 DSS06.06 SR 3.1 SR 3.8 SR 4.1 SR 4.2 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) PR.DS-2 PR.DS-5 FIA_UAU.5 FTP_ITC_EXT.1 FCS_SSH_EXT.1 FCS_SSHS_EXT.1 SRG-OS-000423-GPOS-00187 SRG-OS-000424-GPOS-00188 SRG-OS-000425-GPOS-00189 SRG-OS-000426-GPOS-00190 Without protection of the transmitted information, confidentiality, and integrity may be compromised because unprotected communications can be intercepted and either read or altered. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "openssh-server" ; then dnf install -y "openssh-server" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_openssh-server_installed - name: Ensure openssh-server is installed ansible.builtin.package: name: openssh-server state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_openssh-server_installed include install_openssh-server class install_openssh-server { package { 'openssh-server': ensure => 'installed', } } package --add=openssh-server [[packages]] name = "openssh-server" version = "*" package install openssh-server dnf install openssh-server Remove the OpenSSH Server Package The openssh-server package should be removed. The openssh-server package can be removed with the following command: $ sudo dnf remove openssh-server Without protection of the transmitted information, confidentiality, and integrity may be compromised because unprotected communications can be intercepted and either read or altered. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then # CAUTION: This remediation script will remove openssh-server # from the system, and may remove any packages # that depend on openssh-server. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "openssh-server" ; then dnf remove -y --noautoremove "openssh-server" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_openssh-server_removed - name: 'Remove the OpenSSH Server Package: Ensure openssh-server is removed' ansible.builtin.package: name: openssh-server state: absent when: '"kernel" in ansible_facts.packages' tags: - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_openssh-server_removed include remove_openssh-server class remove_openssh-server { package { 'openssh-server': ensure => 'purged', } } package --remove=openssh-server package remove openssh-server dnf remove openssh-server Disable SSH Server If Possible The SSH server service, sshd, is commonly needed. However, if it can be disabled, do so. This is unusual, as SSH is a common method for encrypted and authenticated remote access. CM-3(6) IA-2(4) SRG-APP-000185-CTR-000490 SRG-APP-000141-CTR-000315 # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then SYSTEMCTL_EXEC='/usr/bin/systemctl' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'sshd.service' fi "$SYSTEMCTL_EXEC" disable 'sshd.service' "$SYSTEMCTL_EXEC" mask 'sshd.service' # Disable socket activation if we have a unit file for it if "$SYSTEMCTL_EXEC" -q list-unit-files sshd.socket; then if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" stop 'sshd.socket' fi "$SYSTEMCTL_EXEC" mask 'sshd.socket' fi # The service may not be running because it has been started and failed, # so let's reset the state so OVAL checks pass. # Service should be 'inactive', not 'failed' after reboot though. "$SYSTEMCTL_EXEC" reset-failed 'sshd.service' || true else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-3(6) - NIST-800-53-IA-2(4) - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - service_sshd_disabled - name: Disable SSH Server If Possible - Disable service sshd block: - name: Disable SSH Server If Possible - Collect systemd Services Present in the System ansible.builtin.command: systemctl -q list-unit-files --type service register: service_exists changed_when: false failed_when: service_exists.rc not in [0, 1] check_mode: false - name: Disable SSH Server If Possible - Ensure sshd.service is Masked ansible.builtin.systemd: name: sshd.service state: stopped enabled: false masked: true when: service_exists.stdout_lines is search("sshd.service", multiline=True) - name: Unit Socket Exists - sshd.socket ansible.builtin.command: systemctl -q list-unit-files sshd.socket register: socket_file_exists changed_when: false failed_when: socket_file_exists.rc not in [0, 1] check_mode: false - name: Disable SSH Server If Possible - Disable Socket sshd ansible.builtin.systemd: name: sshd.socket enabled: false state: stopped masked: true when: socket_file_exists.stdout_lines is search("sshd.socket", multiline=True) tags: - NIST-800-53-CM-3(6) - NIST-800-53-IA-2(4) - disable_strategy - high_severity - low_complexity - low_disruption - no_reboot_needed - service_sshd_disabled - special_service_block when: '"kernel" in ansible_facts.packages' include disable_sshd class disable_sshd { service {'sshd': enable => false, ensure => 'stopped', } } apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: sshd.service enabled: false mask: true - name: sshd.socket enabled: false mask: true [customizations.services] masked = ["sshd"] service disable sshd Verify Group Who Owns SSH Server config file To properly set the group owner of /etc/ssh/sshd_config, run the command: $ sudo chgrp root /etc/ssh/sshd_config 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-17(a) CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 R50 5.1.1 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/etc/ssh/sshd_config" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /etc/ssh/sshd_config fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_groupowner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupowner_sshd_config_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupowner_sshd_config_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_groupowner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/ssh/sshd_config ansible.builtin.stat: path: /etc/ssh/sshd_config register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_groupowner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/ssh/sshd_config ansible.builtin.file: path: /etc/ssh/sshd_config follow: false group: '{{ file_groupowner_sshd_config_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_groupowner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Ownership on SSH Server Private *_key Key Files SSH server private keys, files that match the /etc/ssh/*_key glob, must be group-owned by ssh_keys group. Remediation is not possible at bootable container build time because SSH host keys are generated post-deployment. R50 5.1.2 If an unauthorized user obtains the private SSH host key file, the host could be impersonated. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "ssh_keys" >/dev/null 2>&1; then newgroup="ssh_keys" fi if [[ -z "${newgroup}" ]]; then >&2 echo "ssh_keys is not a defined group on the system" else find -P /etc/ssh/ -maxdepth 1 -type f ! -group ssh_keys -regextype posix-extended -regex '^.*_key$' -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_groupownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Check that the ssh_keys group is defined ansible.builtin.getent: database: group key: ssh_keys ignore_errors: true when: - '"kernel" in ansible_facts.packages' - file_groupownership_sshd_private_key_newgroup is undefined tags: - configure_strategy - file_groupownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupownership_sshd_private_key_newgroup variable if ssh_keys found ansible.builtin.set_fact: file_groupownership_sshd_private_key_newgroup: ssh_keys when: - '"kernel" in ansible_facts.packages' - ansible_facts.getent_group["ssh_keys"] is defined tags: - configure_strategy - file_groupownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/ssh/ file(s) matching ^.*_key$ ansible.builtin.command: find -P /etc/ssh/ -maxdepth 1 -type f ! -group ssh_keys -regextype posix-extended -regex "^.*_key$" register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/ssh/ file(s) matching ^.*_key$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ file_groupownership_sshd_private_key_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Group Ownership on SSH Server Public *.pub Key Files SSH server public keys, files that match the /etc/ssh/*.pub glob, must be group-owned by root group. Remediation is not possible at bootable container build time because SSH host keys are generated post-deployment. R50 5.1.3 If a public host key file is modified by an unauthorized user, the SSH service may be compromised. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /etc/ssh/ -maxdepth 1 -type f ! -group 0 -regextype posix-extended -regex '^.*\.pub$' -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_groupownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupownership_sshd_pub_key_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupownership_sshd_pub_key_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/ssh/ file(s) matching ^.*\.pub$ ansible.builtin.command: find -P /etc/ssh/ -maxdepth 1 -type f ! -group 0 -regextype posix-extended -regex "^.*\.pub$" register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/ssh/ file(s) matching ^.*\.pub$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ file_groupownership_sshd_pub_key_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Owner on SSH Server config file To properly set the owner of /etc/ssh/sshd_config, run the command: $ sudo chown root /etc/ssh/sshd_config 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-17(a) CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 R50 5.1.1 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/etc/ssh/sshd_config" | grep -E -w -q "0"; then chown --no-dereference "$newown" /etc/ssh/sshd_config fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_owner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_owner_sshd_config_newown variable if represented by uid ansible.builtin.set_fact: file_owner_sshd_config_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_owner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/ssh/sshd_config ansible.builtin.stat: path: /etc/ssh/sshd_config register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_owner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/ssh/sshd_config ansible.builtin.file: path: /etc/ssh/sshd_config follow: false owner: '{{ file_owner_sshd_config_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - configure_strategy - file_owner_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Ownership on SSH Server Private *_key Key Files SSH server private keys, files that match the /etc/ssh/*_key glob, must be owned by root user. Remediation is not possible at bootable container build time because SSH host keys are generated post-deployment. R50 5.1.2 If an unauthorized user obtains the private SSH host key file, the host could be impersonated. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/ssh/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex '^.*_key$' -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_ownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_ownership_sshd_private_key_newown variable if represented by uid ansible.builtin.set_fact: file_ownership_sshd_private_key_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/ssh/ file(s) matching ^.*_key$ ansible.builtin.command: find -P /etc/ssh/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex "^.*_key$" register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/ssh/ file(s) matching ^.*_key$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_sshd_private_key_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Ownership on SSH Server Public *.pub Key Files SSH server public keys, files that match the /etc/ssh/*.pub glob, must be owned by root user. Remediation is not possible at bootable container build time because SSH host keys are generated post-deployment. R50 5.1.3 If a public host key file is modified by an unauthorized user, the SSH service may be compromised. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/ssh/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex '^.*\.pub$' -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_ownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_ownership_sshd_pub_key_newown variable if represented by uid ansible.builtin.set_fact: file_ownership_sshd_pub_key_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/ssh/ file(s) matching ^.*\.pub$ ansible.builtin.command: find -P /etc/ssh/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex "^.*\.pub$" register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/ssh/ file(s) matching ^.*\.pub$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_sshd_pub_key_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on SSH Server config file To properly set the permissions of /etc/ssh/sshd_config, run the command: $ sudo chmod 0600 /etc/ssh/sshd_config 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-17(a) CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 R50 5.1.1 2.2.6 2.2 Service configuration files enable or disable features of their respective services that if configured incorrectly can lead to insecure and vulnerable configurations. Therefore, service configuration files should be owned by the correct group to prevent unauthorized changes. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then chmod u-xs,g-xwrs,o-xwrt /etc/ssh/sshd_config else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/ssh/sshd_config ansible.builtin.stat: path: /etc/ssh/sshd_config register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xwrs,o-xwrt on /etc/ssh/sshd_config ansible.builtin.file: path: /etc/ssh/sshd_config mode: u-xs,g-xwrs,o-xwrt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_config - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on SSH Server Private *_key Key Files SSH server private keys - files that match the /etc/ssh/*_key glob, have to have restricted permissions. If those files are owned by the root user and the root group, they have to have the 0600 permission or stricter. If they are owned by the root user, but by a dedicated group ssh_keys, they can have the 0640 permission or stricter. Remediation is not possible at bootable container build time because SSH host keys are generated post-deployment. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.13 3.13.10 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-17(a) CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-2.2.4 SRG-OS-000480-GPOS-00227 R50 5.1.2 2.2.6 2.2 If an unauthorized user obtains the private SSH host key file, the host could be impersonated. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then for keyfile in /etc/ssh/*_key; do test -f "$keyfile" || continue if test root:root = "$(stat -c "%U:%G" "$keyfile")"; then chmod u-xs,g-xwrs,o-xwrt "$keyfile" elif test root:ssh_keys = "$(stat -c "%U:%G" "$keyfile")"; then chmod u-xs,g-xws,o-xwrt "$keyfile" else echo "Key-like file '$keyfile' is owned by an unexpected user:group combination" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find root:root-owned keys ansible.builtin.command: find -H /etc/ssh/ -maxdepth 1 -user root -regex ".*_key$" -type f -group root -perm /u+xs,g+xwrs,o+xwrt register: root_owned_keys changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for root:root-owned keys ansible.builtin.file: path: '{{ item }}' mode: u-xs,g-xwrs,o-xwrt state: file with_items: - '{{ root_owned_keys.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find root:ssh_keys-owned keys ansible.builtin.command: find -H /etc/ssh/ -maxdepth 1 -user root -regex ".*_key$" -type f -group ssh_keys -perm /u+xs,g+xws,o+xwrt register: dedicated_group_owned_keys changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for root:ssh_keys-owned keys ansible.builtin.file: path: '{{ item }}' mode: u-xs,g-xws,o-xwrt state: file with_items: - '{{ dedicated_group_owned_keys.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_private_key - low_complexity - low_disruption - medium_severity - no_reboot_needed include ssh_private_key_perms class ssh_private_key_perms { exec { 'sshd_priv_key': command => "chmod 0640 /etc/ssh/*_key", path => '/bin:/usr/bin' } } Verify Permissions on SSH Server Public *.pub Key Files To properly set the permissions of /etc/ssh/*.pub, run the command: $ sudo chmod 0644 /etc/ssh/*.pub Remediation is not possible at bootable container build time because SSH host keys are generated post-deployment. 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.13 3.13.10 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-17(a) CM-6(a) AC-6(1) PR.AC-4 PR.DS-5 Req-2.2.4 SRG-OS-000480-GPOS-00227 R50 5.1.3 2.2.6 2.2 If a public host key file is modified by an unauthorized user, the SSH service may be compromised. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then find -P /etc/ssh/ -maxdepth 1 -perm /u+xs,g+xws,o+xwt -type f -regextype posix-extended -regex '^.*\.pub$' -exec chmod u-xs,g-xws,o-xwt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/ssh/ file(s) ansible.builtin.command: find -P /etc/ssh/ -maxdepth 1 -perm /u+xs,g+xws,o+xwt -type f -regextype posix-extended -regex "^.*\.pub$" register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/ssh/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-xs,g-xws,o-xwt state: file with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-171-3.13.10 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - configure_strategy - file_permissions_sshd_pub_key - low_complexity - low_disruption - medium_severity - no_reboot_needed include ssh_public_key_perms class ssh_public_key_perms { exec { 'sshd_pub_key': command => "chmod 0644 /etc/ssh/*.pub", path => '/bin:/usr/bin' } } Remove SSH Server iptables Firewall exception (Unusual) By default, inbound connections to SSH's port are allowed. If the SSH server is not being used, this exception should be removed from the firewall configuration. Edit the files /etc/sysconfig/iptables and /etc/sysconfig/ip6tables (if IPv6 is in use). In each file, locate and delete the line: -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT This is unusual, as SSH is a common method for encrypted and authenticated remote access. If inbound SSH connections are not expected, disallowing access to the SSH port will avoid possible exploitation of the port by an attacker. Configure OpenSSH Server if Necessary If the system needs to act as an SSH server, then certain changes should be made to the OpenSSH daemon configuration file /etc/ssh/sshd_config. The following recommendations can be applied to this file. See the sshd_config(5) man page for more detailed information. SSH RekeyLimit - size Specify the size component of the rekey limit. default 512M 512M 1G SSH RekeyLimit - size Specify the size component of the rekey limit. none 1h 1h SSH Compression Setting Specify the compression setting for SSH connections. no delayed no SSH Privilege Separation Setting Specify whether and how sshd separates privileges when handling incoming network connections. no yes sandbox sandbox SSH LoginGraceTime setting Configure parameters for how long the servers stays connected before the user has successfully logged in 60 60 SSH MaxStartups setting Configure parameters for maximum concurrent unauthenticated connections to the SSH daemon. 10:30:100 10:30:60 Set SSH Client Alive Count Max to zero The SSH server sends at most ClientAliveCountMax messages during a SSH session and waits for a response from the SSH client. The option ClientAliveInterval configures timeout after each ClientAliveCountMax message. If the SSH server does not receive a response from the client, then the connection is considered unresponsive and terminated. To ensure the SSH timeout occurs precisely when the ClientAliveInterval is set, set the ClientAliveCountMax to value of 0 in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: 1 12 13 14 15 16 18 3 5 7 8 5.5.6 APO13.01 BAI03.01 BAI03.02 BAI03.03 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.1.11 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 A.12.4.1 A.12.4.3 A.14.1.1 A.14.2.1 A.14.2.5 A.18.1.4 A.6.1.2 A.6.1.5 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.3 CIP-007-3 R5.1 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-2(5) AC-12 AC-17(a) SC-10 CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.IP-2 Req-8.1.8 SRG-OS-000126-GPOS-00066 SRG-OS-000163-GPOS-00072 SRG-OS-000279-GPOS-00109 This ensures a user login will be terminated as soon as the ClientAliveInterval is reached. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "ClientAliveCountMax 0" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_keepalive_0 - name: Set SSH Client Alive Count Max to zero block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter ClientAliveCountMax is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ line: ClientAliveCountMax 0 state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_keepalive_0 - name: Set SSH Client Alive Count Max to zero - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_keepalive_0 Set SSH Client Alive Count Max The SSH server sends at most ClientAliveCountMax messages during a SSH session and waits for a response from the SSH client. The option ClientAliveInterval configures timeout after each ClientAliveCountMax message. If the SSH server does not receive a response from the client, then the connection is considered unresponsive and terminated. For SSH earlier than v8.2, a ClientAliveCountMax value of 0 causes a timeout precisely when the ClientAliveInterval is set. Starting with v8.2, a value of 0 disables the timeout functionality completely. If the option is set to a number greater than 0, then the session will be disconnected after ClientAliveInterval * ClientAliveCountMax seconds without receiving a keep alive message. 1 12 13 14 15 16 18 3 5 7 8 5.5.6 APO13.01 BAI03.01 BAI03.02 BAI03.03 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.1.11 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 A.12.4.1 A.12.4.3 A.14.1.1 A.14.2.1 A.14.2.5 A.18.1.4 A.6.1.2 A.6.1.5 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.3 CIP-007-3 R5.1 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-2(5) AC-12 AC-17(a) SC-10 CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.IP-2 Req-8.1.8 SRG-OS-000163-GPOS-00072 SRG-OS-000279-GPOS-00109 5.1.9 8.2.8 8.2 This ensures a user login will be terminated as soon as the ClientAliveInterval is reached. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_sshd_set_keepalive='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "ClientAliveCountMax $var_sshd_set_keepalive" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_keepalive - name: XCCDF Value var_sshd_set_keepalive # promote to variable set_fact: var_sshd_set_keepalive: !!str tags: - always - name: Set SSH Client Alive Count Max block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter ClientAliveCountMax is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+ line: ClientAliveCountMax {{ var_sshd_set_keepalive }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_keepalive - name: Set SSH Client Alive Count Max - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_keepalive Set SSH Client Alive Interval SSH allows administrators to set a network responsiveness timeout interval. After this interval has passed, the unresponsive client will be automatically logged out. To set this timeout interval, edit the following line in /etc/ssh/sshd_config as follows: ClientAliveInterval The timeout interval is given in seconds. For example, have a timeout of 10 minutes, set interval to 600. If a shorter timeout has already been set for the login shell, that value will preempt any SSH setting made in /etc/ssh/sshd_config. Keep in mind that some processes may stop SSH from correctly detecting that the user is idle. SSH disconnecting unresponsive clients will not have desired effect without also configuring ClientAliveCountMax in the SSH service configuration. Following conditions may prevent the SSH session to time out: Remote processes on the remote machine generates output. As the output has to be transferred over the network to the client, the timeout is reset every time such transfer happens.Any scp or sftp activity by the same user to the host resets the timeout. 1 12 13 14 15 16 18 3 5 7 8 5.5.6 APO13.01 BAI03.01 BAI03.02 BAI03.03 DSS01.03 DSS03.05 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.1.11 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 6.2 A.12.4.1 A.12.4.3 A.14.1.1 A.14.2.1 A.14.2.5 A.18.1.4 A.6.1.2 A.6.1.5 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.3 CIP-007-3 R5.1 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 CM-6(a) AC-17(a) AC-2(5) AC-12 AC-17(a) SC-10 CM-6(a) DE.CM-1 DE.CM-3 PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.IP-2 Req-8.1.8 SRG-OS-000126-GPOS-00066 SRG-OS-000163-GPOS-00072 SRG-OS-000279-GPOS-00109 SRG-OS-000395-GPOS-00175 5.1.9 8.2.8 8.2 Terminating an idle ssh session within a short time period reduces the window of opportunity for unauthorized personnel to take control of a management session enabled on the console or console port that has been let unattended. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then sshd_idle_timeout_value='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*ClientAliveInterval\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*ClientAliveInterval\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*ClientAliveInterval\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "ClientAliveInterval $sshd_idle_timeout_value" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_idle_timeout - name: XCCDF Value sshd_idle_timeout_value # promote to variable set_fact: sshd_idle_timeout_value: !!str tags: - always - name: Set SSH Client Alive Interval block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter ClientAliveInterval is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+ line: ClientAliveInterval {{ sshd_idle_timeout_value }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_idle_timeout - name: Set SSH Client Alive Interval - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.11 - NIST-800-53-AC-12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-17(a) - NIST-800-53-AC-2(5) - NIST-800-53-CM-6(a) - NIST-800-53-CM-6(a) - NIST-800-53-SC-10 - PCI-DSS-Req-8.1.8 - PCI-DSSv4-8.2 - PCI-DSSv4-8.2.8 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_idle_timeout Disable Host-Based Authentication SSH's cryptographic host-based authentication is more secure than .rhosts authentication. However, it is not recommended that hosts unilaterally trust one another, even within an organization. The default SSH configuration disables host-based authentication. The appropriate configuration is used if no value is set for HostbasedAuthentication. To explicitly disable host-based authentication, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: HostbasedAuthentication no 11 12 14 15 16 18 3 5 9 5.5.6 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-3 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.AC-4 PR.AC-6 PR.IP-1 PR.PT-3 FIA_UAU.1 SRG-OS-000480-GPOS-00229 5.1.12 8.3.1 8.3 SSH trust relationships mean a compromise on one host can allow an attacker to move trivially to other hosts. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*HostbasedAuthentication\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*HostbasedAuthentication\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*HostbasedAuthentication\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "HostbasedAuthentication no" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - disable_host_auth - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Host-Based Authentication block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "HostbasedAuthentication"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter HostbasedAuthentication is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "HostbasedAuthentication"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "HostbasedAuthentication"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "HostbasedAuthentication"| regex_escape }}\s+ line: HostbasedAuthentication no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - disable_host_auth - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Disable Host-Based Authentication - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-3 - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-8.3 - PCI-DSSv4-8.3.1 - disable_host_auth - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,%23%09%24OpenBSD%3A%20sshd_config%2Cv%201.103%202018%2F04%2F09%2020%3A41%3A22%20tj%20Exp%20%24%0A%0A%23%20This%20is%20the%20sshd%20server%20system-wide%20configuration%20file.%20%20See%0A%23%20sshd_config%285%29%20for%20more%20information.%0A%0A%23%20This%20sshd%20was%20compiled%20with%20PATH%3D%2Fusr%2Flocal%2Fbin%3A%2Fusr%2Fbin%3A%2Fusr%2Flocal%2Fsbin%3A%2Fusr%2Fsbin%0A%0A%23%20The%20strategy%20used%20for%20options%20in%20the%20default%20sshd_config%20shipped%20with%0A%23%20OpenSSH%20is%20to%20specify%20options%20with%20their%20default%20value%20where%0A%23%20possible%2C%20but%20leave%20them%20commented.%20%20Uncommented%20options%20override%20the%0A%23%20default%20value.%0A%0A%23%20If%20you%20want%20to%20change%20the%20port%20on%20a%20SELinux%20system%2C%20you%20have%20to%20tell%0A%23%20SELinux%20about%20this%20change.%0A%23%20semanage%20port%20-a%20-t%20ssh_port_t%20-p%20tcp%20%23PORTNUMBER%0A%23%0A%23Port%2022%0A%23AddressFamily%20any%0A%23ListenAddress%200.0.0.0%0A%23ListenAddress%20%3A%3A%0A%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_rsa_key%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_ecdsa_key%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_ed25519_key%0A%0A%23%20Ciphers%20and%20keying%0ARekeyLimit%20512M%201h%0A%0A%23%20System-wide%20Crypto%20policy%3A%0A%23%20This%20system%20is%20following%20system-wide%20crypto%20policy.%20The%20changes%20to%0A%23%20Ciphers%2C%20MACs%2C%20KexAlgoritms%20and%20GSSAPIKexAlgorithsm%20will%20not%20have%20any%0A%23%20effect%20here.%20They%20will%20be%20overridden%20by%20command-line%20options%20passed%20on%0A%23%20the%20server%20start%20up.%0A%23%20To%20opt%20out%2C%20uncomment%20a%20line%20with%20redefinition%20of%20%20CRYPTO_POLICY%3D%0A%23%20variable%20in%20%20%2Fetc%2Fsysconfig%2Fsshd%20%20to%20overwrite%20the%20policy.%0A%23%20For%20more%20information%2C%20see%20manual%20page%20for%20update-crypto-policies%288%29.%0A%0A%23%20Logging%0A%23SyslogFacility%20AUTH%0ASyslogFacility%20AUTHPRIV%0A%23LogLevel%20INFO%0A%0A%23%20Authentication%3A%0A%0A%23LoginGraceTime%202m%0APermitRootLogin%20no%0AStrictModes%20yes%0A%23MaxAuthTries%206%0A%23MaxSessions%2010%0A%0APubkeyAuthentication%20yes%0A%0A%23%20The%20default%20is%20to%20check%20both%20.ssh%2Fauthorized_keys%20and%20.ssh%2Fauthorized_keys2%0A%23%20but%20this%20is%20overridden%20so%20installations%20will%20only%20check%20.ssh%2Fauthorized_keys%0AAuthorizedKeysFile%09.ssh%2Fauthorized_keys%0A%0A%23AuthorizedPrincipalsFile%20none%0A%0A%23AuthorizedKeysCommand%20none%0A%23AuthorizedKeysCommandUser%20nobody%0A%0A%23%20For%20this%20to%20work%20you%20will%20also%20need%20host%20keys%20in%20%2Fetc%2Fssh%2Fssh_known_hosts%0AHostbasedAuthentication%20no%0A%23%20Change%20to%20yes%20if%20you%20don%27t%20trust%20~%2F.ssh%2Fknown_hosts%20for%0A%23%20HostbasedAuthentication%0AIgnoreUserKnownHosts%20yes%0A%23%20Don%27t%20read%20the%20user%27s%20~%2F.rhosts%20and%20~%2F.shosts%20files%0AIgnoreRhosts%20yes%0A%0A%23%20To%20disable%20tunneled%20clear%20text%20passwords%2C%20change%20to%20no%20here%21%0A%23PasswordAuthentication%20yes%0APermitEmptyPasswords%20no%0APasswordAuthentication%20no%0A%0A%23%20Change%20to%20no%20to%20disable%20s%2Fkey%20passwords%0A%23ChallengeResponseAuthentication%20yes%0AChallengeResponseAuthentication%20no%0A%0A%23%20Kerberos%20options%0AKerberosAuthentication%20no%0A%23KerberosOrLocalPasswd%20yes%0A%23KerberosTicketCleanup%20yes%0A%23KerberosGetAFSToken%20no%0A%23KerberosUseKuserok%20yes%0A%0A%23%20GSSAPI%20options%0AGSSAPIAuthentication%20no%0AGSSAPICleanupCredentials%20no%0A%23GSSAPIStrictAcceptorCheck%20yes%0A%23GSSAPIKeyExchange%20no%0A%23GSSAPIEnablek5users%20no%0A%0A%23%20Set%20this%20to%20%27yes%27%20to%20enable%20PAM%20authentication%2C%20account%20processing%2C%0A%23%20and%20session%20processing.%20If%20this%20is%20enabled%2C%20PAM%20authentication%20will%0A%23%20be%20allowed%20through%20the%20ChallengeResponseAuthentication%20and%0A%23%20PasswordAuthentication.%20%20Depending%20on%20your%20PAM%20configuration%2C%0A%23%20PAM%20authentication%20via%20ChallengeResponseAuthentication%20may%20bypass%0A%23%20the%20setting%20of%20%22PermitRootLogin%20without-password%22.%0A%23%20If%20you%20just%20want%20the%20PAM%20account%20and%20session%20checks%20to%20run%20without%0A%23%20PAM%20authentication%2C%20then%20enable%20this%20but%20set%20PasswordAuthentication%0A%23%20and%20ChallengeResponseAuthentication%20to%20%27no%27.%0A%23%20WARNING%3A%20%27UsePAM%20no%27%20is%20not%20supported%20in%20Fedora%20and%20may%20cause%20several%0A%23%20problems.%0AUsePAM%20yes%0A%0A%23AllowAgentForwarding%20yes%0A%23AllowTcpForwarding%20yes%0A%23GatewayPorts%20no%0AX11Forwarding%20yes%0A%23X11DisplayOffset%2010%0A%23X11UseLocalhost%20yes%0A%23PermitTTY%20yes%0A%0A%23%20It%20is%20recommended%20to%20use%20pam_motd%20in%20%2Fetc%2Fpam.d%2Fsshd%20instead%20of%20PrintMotd%2C%0A%23%20as%20it%20is%20more%20configurable%20and%20versatile%20than%20the%20built-in%20version.%0APrintMotd%20no%0A%0APrintLastLog%20yes%0A%23TCPKeepAlive%20yes%0APermitUserEnvironment%20no%0ACompression%20no%0AClientAliveInterval%20600%0AClientAliveCountMax%200%0A%23UseDNS%20no%0A%23PidFile%20%2Fvar%2Frun%2Fsshd.pid%0A%23MaxStartups%2010%3A30%3A100%0A%23PermitTunnel%20no%0A%23ChrootDirectory%20none%0A%23VersionAddendum%20none%0A%0A%23%20no%20default%20banner%20path%0ABanner%20%2Fetc%2Fissue%0A%0A%23%20Accept%20locale-related%20environment%20variables%0AAcceptEnv%20LANG%20LC_CTYPE%20LC_NUMERIC%20LC_TIME%20LC_COLLATE%20LC_MONETARY%20LC_MESSAGES%0AAcceptEnv%20LC_PAPER%20LC_NAME%20LC_ADDRESS%20LC_TELEPHONE%20LC_MEASUREMENT%0AAcceptEnv%20LC_IDENTIFICATION%20LC_ALL%20LANGUAGE%0AAcceptEnv%20XMODIFIERS%0A%0A%23%20override%20default%20of%20no%20subsystems%0ASubsystem%09sftp%09%2Fusr%2Flibexec%2Fopenssh%2Fsftp-server%0A%0A%23%20Example%20of%20overriding%20settings%20on%20a%20per-user%20basis%0A%23Match%20User%20anoncvs%0A%23%09X11Forwarding%20no%0A%23%09AllowTcpForwarding%20no%0A%23%09PermitTTY%20no%0A%23%09ForceCommand%20cvs%20server%0A%0AUsePrivilegeSeparation%20sandbox mode: 0600 path: /etc/ssh/sshd_config overwrite: true Allow Only SSH Protocol 2 Only SSH protocol version 2 connections should be permitted. The default setting in /etc/ssh/sshd_config is correct, and can be verified by ensuring that the following line appears: Protocol 2 As of openssh-server version 7.4 and above, the only protocol supported is version 2, and line Protocol 2 in /etc/ssh/sshd_config is not necessary. 1 12 15 16 5 8 5.5.6 APO13.01 DSS01.04 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 3.1.13 3.5.4 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.6 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 0487 1449 1506 A.11.2.6 A.13.1.1 A.13.2.1 A.14.1.3 A.18.1.4 A.6.2.1 A.6.2.2 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CIP-003-8 R4.2 CIP-007-3 R5.1 CIP-007-3 R7.1 CM-6(a) AC-17(a) AC-17(2) IA-5(1)(c) SC-13 MA-4(6) PR.AC-1 PR.AC-3 PR.AC-6 PR.AC-7 PR.PT-4 SRG-OS-000074-GPOS-00042 SRG-OS-000480-GPOS-00227 SSH protocol version 1 is an insecure implementation of the SSH protocol and has many well-known vulnerability exploits. Exploits of the SSH daemon could provide immediate root access to the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*Protocol\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*Protocol\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*Protocol\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "Protocol 2" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.13 - NIST-800-171-3.5.4 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-MA-4(6) - NIST-800-53-SC-13 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_allow_only_protocol2 - name: Allow Only SSH Protocol 2 block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "Protocol"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter Protocol is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "Protocol"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "Protocol"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "Protocol"| regex_escape }}\s+ line: Protocol 2 state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.13 - NIST-800-171-3.5.4 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-MA-4(6) - NIST-800-53-SC-13 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_allow_only_protocol2 - name: Allow Only SSH Protocol 2 - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.13 - NIST-800-171-3.5.4 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1)(c) - NIST-800-53-MA-4(6) - NIST-800-53-SC-13 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_allow_only_protocol2 Disable Compression Or Set Compression to delayed Compression is useful for slow network connections over long distances but can cause performance issues on local LANs. If use of compression is required, it should be enabled only after a user has authenticated; otherwise, it should be disabled. To disable compression or delay compression until after a user has successfully authenticated, add or correct the following line in the /etc/ssh/sshd_config file: Compression 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.IP-1 SRG-OS-000480-GPOS-00227 If compression is allowed in an SSH connection prior to authentication, vulnerabilities in the compression software could result in compromise of the system from an unauthenticated connection, potentially with root privileges. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_sshd_disable_compression='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*Compression\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*Compression\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*Compression\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "Compression $var_sshd_disable_compression" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_compression - name: XCCDF Value var_sshd_disable_compression # promote to variable set_fact: var_sshd_disable_compression: !!str tags: - always - name: Disable Compression Or Set Compression to delayed block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "Compression"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter Compression is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "Compression"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "Compression"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "Compression"| regex_escape }}\s+ line: Compression {{ var_sshd_disable_compression }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_compression - name: Disable Compression Or Set Compression to delayed - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_compression Disable SSH Access via Empty Passwords Disallow SSH login with empty passwords. The default SSH configuration disables logins with empty passwords. The appropriate configuration is used if no value is set for PermitEmptyPasswords. To explicitly disallow SSH login from accounts with empty passwords, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: PermitEmptyPasswords no Any accounts with empty passwords should be disabled immediately, and PAM configuration should prevent users from being able to assign themselves empty passwords. 11 12 13 14 15 16 18 3 5 9 5.5.6 APO01.06 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.02 DSS06.03 DSS06.06 3.1.1 3.1.5 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 5.2 SR 7.6 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.1.2 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.AC-4 PR.AC-6 PR.DS-5 PR.IP-1 PR.PT-3 FIA_UAU.1 Req-2.2.4 SRG-OS-000106-GPOS-00053 SRG-OS-000480-GPOS-00229 SRG-OS-000480-GPOS-00227 5.1.19 2.2.6 2.2 Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*PermitEmptyPasswords\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*PermitEmptyPasswords\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*PermitEmptyPasswords\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "PermitEmptyPasswords no" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_disable_empty_passwords - name: Disable SSH Access via Empty Passwords block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "PermitEmptyPasswords"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter PermitEmptyPasswords is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "PermitEmptyPasswords"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "PermitEmptyPasswords"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "PermitEmptyPasswords"| regex_escape }}\s+ line: PermitEmptyPasswords no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_disable_empty_passwords - name: Disable SSH Access via Empty Passwords - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_disable_empty_passwords Disable GSSAPI Authentication Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like GSSAPI. The default SSH configuration disallows authentications based on GSSAPI. The appropriate configuration is used if no value is set for GSSAPIAuthentication. To explicitly disable GSSAPI authentication, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: GSSAPIAuthentication no 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.4.3.2 4.3.4.3.3 SR 7.6 0418 1055 1402 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 CM-7(a) CM-7(b) CM-6(a) AC-17(a) PR.IP-1 FTP_ITC_EXT.1 FCS_SSH_EXT.1.2 SRG-OS-000364-GPOS-00151 SRG-OS-000480-GPOS-00227 5.1.11 GSSAPI authentication is used to provide additional authentication mechanisms to applications. Allowing GSSAPI authentication through SSH exposes the system's GSSAPI to remote hosts, increasing the attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "GSSAPIAuthentication no" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_gssapi_auth - name: Disable GSSAPI Authentication block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter GSSAPIAuthentication is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ line: GSSAPIAuthentication no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_gssapi_auth - name: Disable GSSAPI Authentication - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_gssapi_auth Disable Kerberos Authentication Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like Kerberos. The default SSH configuration disallows authentication validation through Kerberos. The appropriate configuration is used if no value is set for KerberosAuthentication. To explicitly disable Kerberos authentication, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: KerberosAuthentication no 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.4.3.2 4.3.4.3.3 SR 7.6 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.IP-1 FTP_ITC_EXT.1 FCS_SSH_EXT.1.2 SRG-OS-000364-GPOS-00151 SRG-OS-000480-GPOS-00227 Kerberos authentication for SSH is often implemented using GSSAPI. If Kerberos is enabled through SSH, the SSH daemon provides a means of access to the system's Kerberos implementation. Configuring these settings for the SSH daemon provides additional assurance that remote logon via SSH will not use unused methods of authentication, even in the event of misconfiguration elsewhere. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*KerberosAuthentication\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*KerberosAuthentication\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*KerberosAuthentication\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "KerberosAuthentication no" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_kerb_auth - name: Disable Kerberos Authentication block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "KerberosAuthentication"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter KerberosAuthentication is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "KerberosAuthentication"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "KerberosAuthentication"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "KerberosAuthentication"| regex_escape }}\s+ line: KerberosAuthentication no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_kerb_auth - name: Disable Kerberos Authentication - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_kerb_auth Disable PubkeyAuthentication Authentication Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms. To disable PubkeyAuthentication authentication, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: PubkeyAuthentication no PubkeyAuthentication authentication is used to provide additional authentication mechanisms to applications. Allowing PubkeyAuthentication authentication through SSH allows users to generate their own authentication tokens, increasing the attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*PubkeyAuthentication\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*PubkeyAuthentication\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*PubkeyAuthentication\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "PubkeyAuthentication no" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_pubkey_auth - name: Disable PubkeyAuthentication Authentication block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter PubkeyAuthentication is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ line: PubkeyAuthentication no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_pubkey_auth - name: Disable PubkeyAuthentication Authentication - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_pubkey_auth Disable SSH Support for .rhosts Files SSH can emulate the behavior of the obsolete rsh command in allowing users to enable insecure access to their accounts via .rhosts files. The default SSH configuration disables support for .rhosts. The appropriate configuration is used if no value is set for IgnoreRhosts. To explicitly disable support for .rhosts files, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: IgnoreRhosts yes 11 12 14 15 16 18 3 5 9 5.5.6 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 3.1.12 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.3.2 4.3.4.3.3 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.AC-4 PR.AC-6 PR.IP-1 PR.PT-3 SRG-OS-000480-GPOS-00227 5.1.13 2.2.6 2.2 SSH trust relationships mean a compromise on one host can allow an attacker to move trivially to other hosts. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*IgnoreRhosts\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*IgnoreRhosts\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*IgnoreRhosts\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "IgnoreRhosts yes" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_rhosts - name: Disable SSH Support for .rhosts Files block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "IgnoreRhosts"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter IgnoreRhosts is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "IgnoreRhosts"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "IgnoreRhosts"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "IgnoreRhosts"| regex_escape }}\s+ line: IgnoreRhosts yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_rhosts - name: Disable SSH Support for .rhosts Files - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_rhosts Disable SSH Support for Rhosts RSA Authentication SSH can allow authentication through the obsolete rsh command through the use of the authenticating user's SSH keys. This should be disabled. To ensure this behavior is disabled, add or correct the following line in /etc/ssh/sshd_config: RhostsRSAAuthentication no As of openssh-server version 7.4 and above, the RhostsRSAAuthentication option has been deprecated, and the line RhostsRSAAuthentication no in /etc/ssh/sshd_config is not necessary. 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.IP-1 SRG-OS-000480-GPOS-00227 Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*RhostsRSAAuthentication\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*RhostsRSAAuthentication\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*RhostsRSAAuthentication\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "RhostsRSAAuthentication no" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_rhosts_rsa - name: Disable SSH Support for Rhosts RSA Authentication block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "RhostsRSAAuthentication"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter RhostsRSAAuthentication is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "RhostsRSAAuthentication"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "RhostsRSAAuthentication"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "RhostsRSAAuthentication"| regex_escape }}\s+ line: RhostsRSAAuthentication no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_rhosts_rsa - name: Disable SSH Support for Rhosts RSA Authentication - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_rhosts_rsa Disable SSH Root Login The root user should never be allowed to login to a system directly over a network. To disable root login via SSH, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: PermitRootLogin no 1 11 12 13 14 15 16 18 3 5 5.5.6 APO01.06 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.02 DSS06.03 DSS06.06 DSS06.10 3.1.1 3.1.5 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.18.1.4 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-6(2) AC-17(a) IA-2 IA-2(5) CM-7(a) CM-7(b) CM-6(a) PR.AC-1 PR.AC-4 PR.AC-6 PR.AC-7 PR.DS-5 PR.PT-3 FAU_GEN.1 Req-2.2.4 SRG-OS-000109-GPOS-00056 SRG-OS-000480-GPOS-00227 SRG-APP-000148-CTR-000335 SRG-APP-000190-CTR-000500 R33 5.1.20 2.2.6 2.2 Even though the communications channel may be encrypted, an additional layer of security is gained by extending the policy of not logging directly on as root. In addition, logging in with a user-specific account provides individual accountability of actions performed on the system and also helps to minimize direct attack attempts on root's password. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "PermitRootLogin no" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(2) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-IA-2 - NIST-800-53-IA-2(5) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_root_login - name: Disable SSH Root Login block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter PermitRootLogin is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ line: PermitRootLogin no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(2) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-IA-2 - NIST-800-53-IA-2(5) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_root_login - name: Disable SSH Root Login - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.1 - NIST-800-171-3.1.5 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6(2) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - NIST-800-53-IA-2 - NIST-800-53-IA-2(5) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_root_login Disable SSH root Login with a Password (Insecure) To disable password-based root logins over SSH, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: PermitRootLogin prohibit-password While this disables password-based root logins, direct root logins through other means such as through SSH keys or GSSAPI will still be permitted. Permitting any sort of root login remotely opens up the root account to attack. To fully disable direct root logins over SSH (which is considered a best practice) and prevent remote attacks against the root account, see CCE-27100-7, CCE-27445-6, CCE-80901-2, and similar. Even though the communications channel may be encrypted, an additional layer of security is gained by preventing use of a password. This also helps to minimize direct attack attempts on root's password. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "PermitRootLogin prohibit-password" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_root_password_login - name: Disable SSH root Login with a Password (Insecure) block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter PermitRootLogin is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "PermitRootLogin"| regex_escape }}\s+ line: PermitRootLogin prohibit-password state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_root_password_login - name: Disable SSH root Login with a Password (Insecure) - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_root_password_login Disable SSH TCP Forwarding The AllowTcpForwarding parameter specifies whether TCP forwarding is permitted. To disable TCP forwarding, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: AllowTcpForwarding no 2.2.6 2.2 Leaving port forwarding enabled can expose the organization to security risks and back-doors. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*AllowTcpForwarding\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*AllowTcpForwarding\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*AllowTcpForwarding\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "AllowTcpForwarding no" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_tcp_forwarding - name: Disable SSH TCP Forwarding block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "AllowTcpForwarding"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter AllowTcpForwarding is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "AllowTcpForwarding"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "AllowTcpForwarding"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "AllowTcpForwarding"| regex_escape }}\s+ line: AllowTcpForwarding no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_tcp_forwarding - name: Disable SSH TCP Forwarding - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_tcp_forwarding Disable SSH Support for User Known Hosts SSH can allow system users to connect to systems if a cache of the remote systems public keys is available. This should be disabled. To ensure this behavior is disabled, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: IgnoreUserKnownHosts yes 11 3 9 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.IP-1 SRG-OS-000480-GPOS-00227 Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*IgnoreUserKnownHosts\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*IgnoreUserKnownHosts\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*IgnoreUserKnownHosts\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "IgnoreUserKnownHosts yes" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_user_known_hosts - name: Disable SSH Support for User Known Hosts block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "IgnoreUserKnownHosts"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter IgnoreUserKnownHosts is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "IgnoreUserKnownHosts"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "IgnoreUserKnownHosts"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "IgnoreUserKnownHosts"| regex_escape }}\s+ line: IgnoreUserKnownHosts yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_user_known_hosts - name: Disable SSH Support for User Known Hosts - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_user_known_hosts Disable X11 Forwarding The X11Forwarding parameter provides the ability to tunnel X11 traffic through the connection to enable remote graphic connections. SSH has the capability to encrypt remote X11 connections when SSH's X11Forwarding option is enabled. The default SSH configuration disables X11Forwarding. The appropriate configuration is used if no value is set for X11Forwarding. To explicitly disable X11 Forwarding, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: X11Forwarding no CM-6(b) SRG-OS-000480-GPOS-00227 2.2.6 2.2 Disable X11 forwarding unless there is an operational requirement to use X11 applications directly. There is a small risk that the remote X11 servers of users who are logged in via SSH with X11 forwarding could be compromised by other users on the X11 server. Note that even if X11 forwarding is disabled, users can always install their own forwarders. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "X11Forwarding no" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_x11_forwarding - name: Disable X11 Forwarding block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter X11Forwarding is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ line: X11Forwarding no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_x11_forwarding - name: Disable X11 Forwarding - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_disable_x11_forwarding Do Not Allow SSH Environment Options Ensure that users are not able to override environment variables of the SSH daemon. The default SSH configuration disables environment processing. The appropriate configuration is used if no value is set for PermitUserEnvironment. To explicitly disable Environment options, add or correct the following /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: PermitUserEnvironment no 11 3 9 5.5.6 BAI10.01 BAI10.02 BAI10.03 BAI10.05 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.4.3.2 4.3.4.3.3 SR 7.6 A.12.1.2 A.12.5.1 A.12.6.2 A.14.2.2 A.14.2.3 A.14.2.4 AC-17(a) CM-7(a) CM-7(b) CM-6(a) PR.IP-1 Req-2.2.4 SRG-OS-000480-GPOS-00229 5.1.21 2.2.6 2.2 SSH environment options potentially allow users to bypass access restriction in some configurations. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*PermitUserEnvironment\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*PermitUserEnvironment\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*PermitUserEnvironment\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "PermitUserEnvironment no" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_do_not_permit_user_env - name: Do Not Allow SSH Environment Options block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "PermitUserEnvironment"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter PermitUserEnvironment is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "PermitUserEnvironment"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "PermitUserEnvironment"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "PermitUserEnvironment"| regex_escape }}\s+ line: PermitUserEnvironment no state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_do_not_permit_user_env - name: Do Not Allow SSH Environment Options - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_do_not_permit_user_env Enable GSSAPI Authentication Sites setup to use Kerberos or other GSSAPI Authenticaion require setting sshd to accept this authentication. To enable GSSAPI authentication, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: GSSAPIAuthentication yes Kerberos authentication for SSH is often implemented using GSSAPI. If Kerberos is enabled through SSH, the SSH daemon provides a means of access to the system's Kerberos implementation. Vulnerabilities in the system's Kerberos implementations may be subject to exploitation. For enterprises, Kerberos is often enabled and used with GSSAPI for centralized user account management which may necessitate enabling of GSSAPI functionality in SSH. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "GSSAPIAuthentication yes" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_gssapi_auth - name: Enable GSSAPI Authentication block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter GSSAPIAuthentication is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+ line: GSSAPIAuthentication yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_gssapi_auth - name: Enable GSSAPI Authentication - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_gssapi_auth Enable PAM UsePAM Enables the Pluggable Authentication Module interface. If set to “yes” this will enable PAM authentication using ChallengeResponseAuthentication and PasswordAuthentication in addition to PAM account and session module processing for all authentication types. To enable PAM authentication, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: UsePAM yes SRG-OS-000125-GPOS-00065 5.1.22 2.2.6 2.2 When UsePAM is set to yes, PAM runs through account and session types properly. This is important if you want to restrict access to services based off of IP, time or other factors of the account. Additionally, you can make sure users inherit certain environment variables on login or disallow access to the server. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*UsePAM\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*UsePAM\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*UsePAM\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "UsePAM yes" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_pam - name: Enable PAM block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "UsePAM"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter UsePAM is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "UsePAM"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "UsePAM"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "UsePAM"| regex_escape }}\s+ line: UsePAM yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_pam - name: Enable PAM - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_pam Enable Public Key Authentication Enable SSH login with public keys. The default SSH configuration enables authentication based on public keys. The appropriate configuration is used if no value is set for PubkeyAuthentication. To explicitly enable Public Key Authentication, add or correct the following /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: PubkeyAuthentication yes SRG-OS-000105-GPOS-00052 SRG-OS-000106-GPOS-00053 SRG-OS-000107-GPOS-00054 SRG-OS-000108-GPOS-00055 Without the use of multifactor authentication, the ease of access to privileged functions is greatly increased. Multifactor authentication requires using two or more factors to achieve authentication. A privileged account is defined as an information system account with authorizations of a privileged user. Smart cards or hardware tokens paired with digital certificates are common examples of multifactor implementations. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*PubkeyAuthentication\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*PubkeyAuthentication\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*PubkeyAuthentication\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "PubkeyAuthentication yes" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_pubkey_auth - name: Enable Public Key Authentication block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter PubkeyAuthentication is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "PubkeyAuthentication"| regex_escape }}\s+ line: PubkeyAuthentication yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_pubkey_auth - name: Enable Public Key Authentication - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_pubkey_auth Enable Use of Strict Mode Checking SSHs StrictModes option checks file and ownership permissions in the user's home directory .ssh folder before accepting login. If world- writable permissions are found, logon is rejected. The default SSH configuration has StrictModes enabled. The appropriate configuration is used if no value is set for StrictModes. To explicitly enable StrictModes in SSH, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: StrictModes yes 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 AC-6 AC-17(a) CM-6(a) PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 If other users have access to modify user-specific SSH configuration files, they may be able to log into the system as another user. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*StrictModes\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*StrictModes\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*StrictModes\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "StrictModes yes" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_strictmodes - name: Enable Use of Strict Mode Checking block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "StrictModes"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter StrictModes is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "StrictModes"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "StrictModes"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "StrictModes"| regex_escape }}\s+ line: StrictModes yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_strictmodes - name: Enable Use of Strict Mode Checking - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_strictmodes Enable SSH Warning Banner To enable the warning banner and ensure it is consistent across the system, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: Banner /etc/issue Another section contains information on how to create an appropriate system-wide warning banner. 1 12 15 16 5.5.6 DSS05.04 DSS05.10 DSS06.10 3.1.9 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-8(a) AC-8(c) AC-17(a) CM-6(a) PR.AC-7 FTA_TAB.1 Req-2.2.4 SRG-OS-000023-GPOS-00006 SRG-OS-000228-GPOS-00088 The warning message reinforces policy awareness during the logon process and facilitates possible legal action against attackers. Alternatively, systems whose ownership should not be obvious should ensure usage of a banner that does not provide easy attribution. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "Banner /etc/issue" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.9 - NIST-800-53-AC-17(a) - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_warning_banner - name: Enable SSH Warning Banner block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter Banner is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "Banner"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+ line: Banner /etc/issue state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.9 - NIST-800-53-AC-17(a) - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_warning_banner - name: Enable SSH Warning Banner - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.9 - NIST-800-53-AC-17(a) - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_warning_banner Enable SSH Warning Banner To enable the warning banner and ensure it is consistent across the system, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: Banner /etc/issue.net Another section contains information on how to create an appropriate system-wide warning banner. 5.5.6 DSS05.04 DSS05.10 DSS06.10 3.1.9 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-8(a) AC-8(c) AC-17(a) CM-6(a) PR.AC-7 SRG-OS-000023-GPOS-00006 SRG-OS-000228-GPOS-00088 5.1.8 The warning message reinforces policy awareness during the logon process and facilitates possible legal action against attackers. Alternatively, systems whose ownership should not be obvious should ensure usage of a banner that does not provide easy attribution. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "Banner /etc/issue.net" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.5.6 - NIST-800-171-3.1.9 - NIST-800-53-AC-17(a) - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_warning_banner_net - name: Enable SSH Warning Banner block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter Banner is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "Banner"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+ line: Banner /etc/issue.net state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.9 - NIST-800-53-AC-17(a) - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_warning_banner_net - name: Enable SSH Warning Banner - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - CJIS-5.5.6 - NIST-800-171-3.1.9 - NIST-800-53-AC-17(a) - NIST-800-53-AC-8(a) - NIST-800-53-AC-8(c) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_enable_warning_banner_net Enable Encrypted X11 Forwarding By default, remote X11 connections are not encrypted when initiated by users. SSH has the capability to encrypt remote X11 connections when SSH's X11Forwarding option is enabled. To enable X11 Forwarding, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: X11Forwarding yes 1 11 12 13 15 16 18 20 3 4 6 9 BAI03.08 BAI07.04 BAI10.01 BAI10.02 BAI10.03 BAI10.05 DSS03.01 3.1.13 4.3.4.3.2 4.3.4.3.3 4.4.3.3 SR 7.6 A.12.1.1 A.12.1.2 A.12.1.4 A.12.5.1 A.12.6.2 A.13.1.1 A.13.1.2 A.14.2.2 A.14.2.3 A.14.2.4 CIP-007-3 R7.1 CM-6(a) AC-17(a) AC-17(2) DE.AE-1 PR.DS-7 PR.IP-1 SRG-OS-000480-GPOS-00227 Non-encrypted X displays allow an attacker to capture keystrokes and to execute commands remotely. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "X11Forwarding yes" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.13 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_enable_x11_forwarding - name: Enable Encrypted X11 Forwarding block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter X11Forwarding is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "X11Forwarding"| regex_escape }}\s+ line: X11Forwarding yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_enable_x11_forwarding - name: Enable Encrypted X11 Forwarding - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.13 - NIST-800-53-AC-17(2) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - high_severity - low_complexity - low_disruption - no_reboot_needed - restrict_strategy - sshd_enable_x11_forwarding Limit Users' SSH Access By default, the SSH configuration allows any user with an account to access the system. There are several options available to limit which users and group can access the system via SSH. It is recommended that at least one of the following options be leveraged: - AllowUsers variable gives the system administrator the option of allowing specific users to ssh into the system. The list consists of space separated user names. Numeric user IDs are not recognized with this variable. If a system administrator wants to restrict user access further by specifically allowing a user's access only from a particular host, the entry can be specified in the form of user@host. - AllowGroups variable gives the system administrator the option of allowing specific groups of users to ssh into the system. The list consists of space separated group names. Numeric group IDs are not recognized with this variable. - DenyUsers variable gives the system administrator the option of denying specific users to ssh into the system. The list consists of space separated user names. Numeric user IDs are not recognized with this variable. If a system administrator wants to restrict user access further by specifically denying a user's access from a particular host, the entry can be specified in the form of user@host. - DenyGroups variable gives the system administrator the option of denying specific groups of users to ssh into the system. The list consists of space separated group names. Numeric group IDs are not recognized with this variable. Automated remediation is not available for this configuration check because each system has unique user names and group names. 11 12 14 15 16 18 3 5 DSS05.02 DSS05.04 DSS05.05 DSS05.07 DSS06.03 DSS06.06 3.1.12 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.5.3 4.3.3.5.4 4.3.3.5.5 4.3.3.5.6 4.3.3.5.7 4.3.3.5.8 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.1 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.11 SR 1.12 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.6 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.2 SR 2.3 SR 2.4 SR 2.5 SR 2.6 SR 2.7 A.6.1.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 AC-3 CM-6(a) PR.AC-4 PR.AC-6 PR.PT-3 Req-2.2.4 5.1.7 2.2.6 2.2 Specifying which accounts are allowed SSH access into the system reduces the possibility of unauthorized access to the system. Enable SSH Print Last Log Ensure that SSH will display the date and time of the last successful account logon. The default SSH configuration enables print of the date and time of the last login. The appropriate configuration is used if no value is set for PrintLastLog. To explicitly enable LastLog in SSH, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: PrintLastLog yes 1 12 15 16 DSS05.04 DSS05.10 DSS06.10 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 SR 1.1 SR 1.10 SR 1.2 SR 1.5 SR 1.7 SR 1.8 SR 1.9 A.18.1.4 A.9.2.1 A.9.2.4 A.9.3.1 A.9.4.2 A.9.4.3 AC-9 AC-9(1) PR.AC-7 SRG-OS-000480-GPOS-00227 Providing users feedback on when account accesses last occurred facilitates user recognition and reporting of unauthorized account use. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*PrintLastLog\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*PrintLastLog\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*PrintLastLog\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "PrintLastLog yes" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_print_last_log - name: Enable SSH Print Last Log block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "PrintLastLog"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter PrintLastLog is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "PrintLastLog"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "PrintLastLog"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "PrintLastLog"| regex_escape }}\s+ line: PrintLastLog yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_print_last_log - name: Enable SSH Print Last Log - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-9 - NIST-800-53-AC-9(1) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_print_last_log Force frequent session key renegotiation The RekeyLimit parameter specifies how often the session key of the is renegotiated, both in terms of amount of data that may be transmitted and the time elapsed. To decrease the default limits, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: RekeyLimit FCS_SSH_EXT.1.8 SRG-OS-000480-GPOS-00227 SRG-OS-000033-GPOS-00014 By decreasing the limit based on the amount of data and enabling time-based limit, effects of potential attacks against encryption keys are limited. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_rekey_limit_size='' var_rekey_limit_time='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*RekeyLimit\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*RekeyLimit\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*RekeyLimit\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "RekeyLimit $var_rekey_limit_size $var_rekey_limit_time" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sshd_rekey_limit - name: XCCDF Value var_rekey_limit_size # promote to variable set_fact: var_rekey_limit_size: !!str tags: - always - name: XCCDF Value var_rekey_limit_time # promote to variable set_fact: var_rekey_limit_time: !!str tags: - always - name: Force frequent session key renegotiation block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "RekeyLimit"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter RekeyLimit is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "RekeyLimit"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "RekeyLimit"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "RekeyLimit"| regex_escape }}\s+ line: RekeyLimit {{ var_rekey_limit_size }} {{ var_rekey_limit_time }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sshd_rekey_limit - name: Force frequent session key renegotiation - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - sshd_rekey_limit Ensure SSH LoginGraceTime is configured The LoginGraceTime parameter to the SSH server specifies the time allowed for successful authentication to the SSH server. The longer the Grace period is the more open unauthenticated connections can exist. Like other session controls in this session the Grace Period should be limited to appropriate limits to ensure the service is available for needed access. 5.1.14 2.2.6 2.2 Setting the LoginGraceTime parameter to a low number will minimize the risk of successful brute force attacks to the SSH server. It will also limit the number of concurrent unauthenticated connections. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_sshd_set_login_grace_time='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*LoginGraceTime\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*LoginGraceTime\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*LoginGraceTime\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "LoginGraceTime $var_sshd_set_login_grace_time" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_login_grace_time - name: XCCDF Value var_sshd_set_login_grace_time # promote to variable set_fact: var_sshd_set_login_grace_time: !!str tags: - always - name: Ensure SSH LoginGraceTime is configured block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "LoginGraceTime"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter LoginGraceTime is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "LoginGraceTime"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "LoginGraceTime"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "LoginGraceTime"| regex_escape }}\s+ line: LoginGraceTime {{ var_sshd_set_login_grace_time }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_login_grace_time - name: Ensure SSH LoginGraceTime is configured - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_login_grace_time Set LogLevel to INFO The INFO parameter specifices that record login and logout activity will be logged. The default SSH configuration sets the log level to INFO. The appropriate configuration is used if no value is set for LogLevel. To explicitly specify the log level in SSH, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: LogLevel INFO AC-17(a) CM-6(a) SSH provides several logging levels with varying amounts of verbosity. DEBUG is specifically not recommended other than strictly for debugging SSH communications since it provides so much data that it is difficult to identify important security information. INFO level is the basic level that only records login activity of SSH users. In many situations, such as Incident Response, it is important to determine when a particular user was active on a system. The logout record can eliminate those users who disconnected, which helps narrow the field. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*LogLevel\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*LogLevel\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*LogLevel\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "LogLevel INFO" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sshd_set_loglevel_info - name: Set LogLevel to INFO block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "LogLevel"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter LogLevel is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "LogLevel"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "LogLevel"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "LogLevel"| regex_escape }}\s+ line: LogLevel INFO state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sshd_set_loglevel_info - name: Set LogLevel to INFO - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sshd_set_loglevel_info Set SSH Daemon LogLevel to VERBOSE The VERBOSE parameter configures the SSH daemon to record login and logout activity. To specify the log level in SSH, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: LogLevel VERBOSE CIP-007-3 R7.1 AC-17(a) AC-17(1) CM-6(a) Req-2.2.4 SRG-OS-000032-GPOS-00013 5.1.15 2.2.6 2.2 SSH provides several logging levels with varying amounts of verbosity. DEBUG is specifically not recommended other than strictly for debugging SSH communications since it provides so much data that it is difficult to identify important security information. INFO or VERBOSE level is the basic level that only records login activity of SSH users. In many situations, such as Incident Response, it is important to determine when a particular user was active on a system. The logout record can eliminate those users who disconnected, which helps narrow the field. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*LogLevel\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*LogLevel\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*LogLevel\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "LogLevel VERBOSE" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-17(1) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_loglevel_verbose - name: Set SSH Daemon LogLevel to VERBOSE block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "LogLevel"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter LogLevel is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "LogLevel"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "LogLevel"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "LogLevel"| regex_escape }}\s+ line: LogLevel VERBOSE state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(1) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_loglevel_verbose - name: Set SSH Daemon LogLevel to VERBOSE - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17(1) - NIST-800-53-AC-17(a) - NIST-800-53-CM-6(a) - PCI-DSS-Req-2.2.4 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_loglevel_verbose Set SSH authentication attempt limit The MaxAuthTries parameter specifies the maximum number of authentication attempts permitted per connection. Once the number of failures reaches half this value, additional failures are logged. to set MaxAUthTries edit /etc/ssh/sshd_config as follows: MaxAuthTries 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 5.1.16 2.2.6 2.2 Setting the MaxAuthTries parameter to a low number will minimize the risk of successful brute force attacks to the SSH server. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then sshd_max_auth_tries_value='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*MaxAuthTries\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*MaxAuthTries\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*MaxAuthTries\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "MaxAuthTries $sshd_max_auth_tries_value" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_max_auth_tries - name: XCCDF Value sshd_max_auth_tries_value # promote to variable set_fact: sshd_max_auth_tries_value: !!str tags: - always - name: Set SSH authentication attempt limit block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "MaxAuthTries"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter MaxAuthTries is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "MaxAuthTries"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "MaxAuthTries"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "MaxAuthTries"| regex_escape }}\s+ line: MaxAuthTries {{ sshd_max_auth_tries_value }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_max_auth_tries - name: Set SSH authentication attempt limit - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_max_auth_tries Set SSH MaxSessions limit The MaxSessions parameter specifies the maximum number of open sessions permitted from a given connection. To set MaxSessions edit /etc/ssh/sshd_config as follows: MaxSessions 5.1.18 2.2.6 2.2 To protect a system from denial of service due to a large number of concurrent sessions, use the rate limiting function of MaxSessions to protect availability of sshd logins and prevent overwhelming the daemon. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_sshd_max_sessions='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*MaxSessions\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*MaxSessions\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*MaxSessions\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "MaxSessions $var_sshd_max_sessions" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_max_sessions - name: XCCDF Value var_sshd_max_sessions # promote to variable set_fact: var_sshd_max_sessions: !!str tags: - always - name: Set SSH MaxSessions limit block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "MaxSessions"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter MaxSessions is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "MaxSessions"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "MaxSessions"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "MaxSessions"| regex_escape }}\s+ line: MaxSessions {{ var_sshd_max_sessions }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_max_sessions - name: Set SSH MaxSessions limit - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_max_sessions Ensure SSH MaxStartups is configured The MaxStartups parameter specifies the maximum number of concurrent unauthenticated connections to the SSH daemon. Additional connections will be dropped until authentication succeeds or the LoginGraceTime expires for a connection. To configure MaxStartups, you should add or edit the following line in the /etc/ssh/sshd_config file: MaxStartups 5.1.17 2.2.6 2.2 To protect a system from denial of service due to a large number of pending authentication connection attempts, use the rate limiting function of MaxStartups to protect availability of sshd logins and prevent overwhelming the daemon. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_sshd_set_maxstartups='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*MaxStartups\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*MaxStartups\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*MaxStartups\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "MaxStartups $var_sshd_set_maxstartups" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_maxstartups - name: XCCDF Value var_sshd_set_maxstartups # promote to variable set_fact: var_sshd_set_maxstartups: !!str tags: - always - name: Ensure SSH MaxStartups is configured block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "MaxStartups"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter MaxStartups is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "MaxStartups"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "MaxStartups"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "MaxStartups"| regex_escape }}\s+ line: MaxStartups {{ var_sshd_set_maxstartups }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_maxstartups - name: Ensure SSH MaxStartups is configured - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.6 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_set_maxstartups Distribute the SSH Server configuration to multiple files in a config directory. Make sure to have the Include /etc/ssh/sshd_config.d/*.conf line in the /etc/ssh/sshd_config file. Ideally, don't have any active configuration directives in that file, and distribute the service configuration to several files in the /etc/ssh/sshd_config.d directory. 164.312(a) FCS_SSH_EXT.1 This form of distributed configuration is considered as a good practice, and as other sshd rules assume that directives in files in the /etc/ssh/sshd_config.d config directory are effective, there has to be a rule that ensures this. Aside from that, having multiple configuration files makes the SSH Server configuration changes easier to partition according to the reason that they were introduced, and therefore it should help to perform merges of hardening updates. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if test -f /etc/ssh/sshd_config.d/sshd_config_original.conf; then printf '%s\n' "Remediation probably already happened, '/etc/ssh/sshd_config.d/sshd_config_original.conf' already exists, not doing anything." >&2 false 1 elif grep -Eq '^\s*Include\s+/etc/ssh/sshd_config\.d/\*\.conf' /etc/ssh/sshd_config && ! grep -Eq '^\s*Match\s' /etc/ssh/sshd_config; then printf '%s\n' "Remediation probably already happened, '/etc/ssh/sshd_config' already contains the include directive." >&2 false 1 else mkdir -p /etc/ssh/sshd_config.d mv /etc/ssh/sshd_config /etc/ssh/sshd_config.d/sshd_config_original.conf cat > /etc/ssh/sshd_config << EOF # To modify the system-wide sshd configuration, create a *.conf file under # /etc/ssh/sshd_config.d/ which will be automatically included below Include /etc/ssh/sshd_config.d/*.conf EOF fi else >&2 echo 'Remediation is not applicable, nothing was done' fi Enable Use of Privilege Separation When enabled, SSH will create an unprivileged child process that has the privilege of the authenticated user. To enable privilege separation in SSH, add or correct the following line in the /etc/ssh/sshd_config file: UsePrivilegeSeparation 12 13 14 15 16 18 3 5 APO01.06 DSS05.04 DSS05.07 DSS06.02 3.1.12 164.308(a)(4)(i) 164.308(b)(1) 164.308(b)(3) 164.310(b) 164.312(e)(1) 164.312(e)(2)(ii) 4.3.3.7.3 SR 2.1 SR 5.2 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-17(a) AC-6 PR.AC-4 PR.DS-5 SRG-OS-000480-GPOS-00227 SSH daemon privilege separation causes the SSH process to drop root privileges when not needed which would decrease the impact of software vulnerabilities in the unprivileged section. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then var_sshd_priv_separation='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*UsePrivilegeSeparation\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*UsePrivilegeSeparation\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*UsePrivilegeSeparation\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "UsePrivilegeSeparation $var_sshd_priv_separation" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_priv_separation - name: XCCDF Value var_sshd_priv_separation # promote to variable set_fact: var_sshd_priv_separation: !!str tags: - always - name: Enable Use of Privilege Separation block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "UsePrivilegeSeparation"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter UsePrivilegeSeparation is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "UsePrivilegeSeparation"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "UsePrivilegeSeparation"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "UsePrivilegeSeparation"| regex_escape }}\s+ line: UsePrivilegeSeparation {{ var_sshd_priv_separation }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_priv_separation - name: Enable Use of Privilege Separation - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.12 - NIST-800-53-AC-17(a) - NIST-800-53-AC-6 - NIST-800-53-CM-6(a) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_priv_separation Use Only Strong Key Exchange algorithms Limit the Key Exchange to strong algorithms. The following line in /etc/ssh/sshd_config demonstrates use of those: KexAlgorithms Req-2.3 5.1.5 2.2.7 2.2 Key exchange is any method in cryptography by which cryptographic keys are exchanged between two parties, allowing use of a cryptographic algorithm. If the sender and receiver wish to exchange encrypted messages, each must be equipped to encrypt messages to be sent and decrypt messages received # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then sshd_strong_kex='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*KexAlgorithms\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*KexAlgorithms\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*KexAlgorithms\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "KexAlgorithms $sshd_strong_kex" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-2.3 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.7 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_kex - name: XCCDF Value sshd_strong_kex # promote to variable set_fact: sshd_strong_kex: !!str tags: - always - name: Use Only Strong Key Exchange algorithms block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "KexAlgorithms"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter KexAlgorithms is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "KexAlgorithms"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "KexAlgorithms"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "KexAlgorithms"| regex_escape }}\s+ line: KexAlgorithms {{ sshd_strong_kex }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-2.3 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.7 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_kex - name: Use Only Strong Key Exchange algorithms - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-2.3 - PCI-DSSv4-2.2 - PCI-DSSv4-2.2.7 - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_kex Use Only Strong MACs Limit the MACs to strong hash algorithms. The following line in /etc/ssh/sshd_config demonstrates use of those MACs: MACs AC-17 (2) SRG-OS-000250-GPOS-00093 5.1.6 MD5 and 96-bit MAC algorithms are considered weak and have been shown to increase exploitability in SSH downgrade attacks. Weak algorithms continue to have a great deal of attention as a weak spot that can be exploited with expanded computing power. An attacker that breaks the algorithm could take advantage of a MiTM position to decrypt the SSH tunnel and capture credentials and information # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then sshd_strong_macs='' mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf LC_ALL=C sed -i "/^\s*MACs\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*MACs\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then LC_ALL=C sed -i "/^\s*MACs\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" else touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" # Insert at the beginning of the file printf '%s\n' "MACs $sshd_strong_macs" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-17 (2) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_macs - name: XCCDF Value sshd_strong_macs # promote to variable set_fact: sshd_strong_macs: !!str tags: - always - name: Use Only Strong MACs block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "MACs"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter MACs is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "MACs"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "MACs"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf create: true regexp: (?i)(?i)^\s*{{ "MACs"| regex_escape }}\s+ line: MACs {{ sshd_strong_macs }} state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17 (2) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_macs - name: Use Only Strong MACs - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-17 (2) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_macs SSH server uses strong entropy to seed To set up SSH server to use entropy from a high-quality source, edit the /etc/sysconfig/sshd file. The SSH_USE_STRONG_RNG configuration value determines how many bytes of entropy to use, so make sure that the file contains line SSH_USE_STRONG_RNG=32 This setting can cause problems on computers without the hardware random generator, because insufficient entropy causes the connection to be blocked until enough entropy is available. SRG-OS-000480-GPOS-00232 SRG-OS-000480-GPOS-00227 SSH implementation in Fedora uses the openssl library, which doesn't use high-entropy sources by default. Randomness is needed to generate data-encryption keys, and as plaintext padding and initialization vectors in encryption algorithms, and high-quality entropy elliminates the possibility that the output of the random number generator used by SSH would be known to potential attackers. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if [ -e "/etc/sysconfig/sshd" ] ; then LC_ALL=C sed -i "/^\s*SSH_USE_STRONG_RNG\s*=\s*/d" "/etc/sysconfig/sshd" else touch "/etc/sysconfig/sshd" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/sysconfig/sshd" cp "/etc/sysconfig/sshd" "/etc/sysconfig/sshd.bak" # Insert before the line matching the regex '^#\s*SSH_USE_STRONG_RNG'. line_number="$(LC_ALL=C grep -n "^#\s*SSH_USE_STRONG_RNG" "/etc/sysconfig/sshd.bak" | LC_ALL=C sed 's/:.*//g')" if [ -z "$line_number" ]; then # There was no match of '^#\s*SSH_USE_STRONG_RNG', insert at # the end of the file. printf '%s\n' "SSH_USE_STRONG_RNG=32" >> "/etc/sysconfig/sshd" else head -n "$(( line_number - 1 ))" "/etc/sysconfig/sshd.bak" > "/etc/sysconfig/sshd" printf '%s\n' "SSH_USE_STRONG_RNG=32" >> "/etc/sysconfig/sshd" tail -n "+$(( line_number ))" "/etc/sysconfig/sshd.bak" >> "/etc/sysconfig/sshd" fi # Clean up after ourselves. rm "/etc/sysconfig/sshd.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_rng - name: Setting unquoted shell-style assignment of 'SSH_USE_STRONG_RNG' to '32' in '/etc/sysconfig/sshd' block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/sysconfig/sshd create: true regexp: (?i)^\s*SSH_USE_STRONG_RNG= state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/sysconfig/sshd ansible.builtin.lineinfile: path: /etc/sysconfig/sshd create: true regexp: (?i)^\s*SSH_USE_STRONG_RNG= state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/sysconfig/sshd ansible.builtin.lineinfile: path: /etc/sysconfig/sshd create: true regexp: (?i)^\s*SSH_USE_STRONG_RNG= line: SSH_USE_STRONG_RNG=32 state: present insertbefore: ^# SSH_USE_STRONG_RNG validate: /usr/bin/bash -n %s when: '"kernel" in ansible_facts.packages' tags: - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - sshd_use_strong_rng Prevent remote hosts from connecting to the proxy display The SSH daemon should prevent remote hosts from connecting to the proxy display. The default SSH configuration for X11UseLocalhost is yes, which prevents remote hosts from connecting to the proxy display. To explicitly prevent remote connections to the proxy display, add or correct the following line in /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf: X11UseLocalhost yes CM-6(b) SRG-OS-000480-GPOS-00227 When X11 forwarding is enabled, there may be additional exposure to the server and client displays if the sshd proxy display is configured to listen on the wildcard address. By default, sshd binds the forwarding server to the loopback address and sets the hostname part of the DISPLAY environment variable to localhost. This prevents remote hosts from connecting to the proxy display. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then mkdir -p /etc/ssh/sshd_config.d touch /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf chmod 0600 /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf LC_ALL=C sed -i "/^\s*X11UseLocalhost\s\+/Id" "/etc/ssh/sshd_config" LC_ALL=C sed -i "/^\s*X11UseLocalhost\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf if [ -e "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" ] ; then LC_ALL=C sed -i "/^\s*X11UseLocalhost\s\+/Id" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" else touch "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cp "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" # Insert at the beginning of the file printf '%s\n' "X11UseLocalhost yes" > "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" cat "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" >> "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf" # Clean up after ourselves. rm "/etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_x11_use_localhost - name: Prevent remote hosts from connecting to the proxy display block: - name: Deduplicate values from /etc/ssh/sshd_config ansible.builtin.lineinfile: path: /etc/ssh/sshd_config create: false regexp: (?i)(?i)^\s*{{ "X11UseLocalhost"| regex_escape }}\s+ state: absent - name: Check if /etc/ssh/sshd_config.d exists ansible.builtin.stat: path: /etc/ssh/sshd_config.d register: _etc_ssh_sshd_config_d_exists - name: Check if the parameter X11UseLocalhost is present in /etc/ssh/sshd_config.d ansible.builtin.find: paths: /etc/ssh/sshd_config.d recurse: 'yes' follow: 'no' contains: (?i)^\s*{{ "X11UseLocalhost"| regex_escape }}\s+ register: _etc_ssh_sshd_config_d_has_parameter when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir - name: Remove parameter from files in /etc/ssh/sshd_config.d ansible.builtin.lineinfile: path: '{{ item.path }}' create: false regexp: (?i)(?i)^\s*{{ "X11UseLocalhost"| regex_escape }}\s+ state: absent with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}' when: _etc_ssh_sshd_config_d_has_parameter.matched - name: Insert correct line to /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.lineinfile: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf create: true regexp: (?i)(?i)^\s*{{ "X11UseLocalhost"| regex_escape }}\s+ line: X11UseLocalhost yes state: present insertbefore: BOF validate: /usr/sbin/sshd -t -f %s when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_x11_use_localhost - name: Prevent remote hosts from connecting to the proxy display - set file mode for /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf ansible.builtin.file: path: /etc/ssh/sshd_config.d/01-complianceascode-reinforce-os-defaults.conf mode: '0600' state: touch when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(b) - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - sshd_x11_use_localhost System Security Services Daemon The System Security Services Daemon (SSSD) is a system daemon that provides access to different identity and authentication providers such as Red Hat's IdM, Microsoft's AD, openLDAP, MIT Kerberos, etc. It uses a common framework that can provide caching and offline support to systems utilizing SSSD. SSSD using caching to reduce load on authentication servers permit offline authentication as well as store extended user data. For more information, see SSSD certificate_verification option Value of the certificate_verification option in the SSSD config. sha1 sha256 sha384 sha512 sha1 SSSD memcache_timeout option Value of the memcache_timeout option in the [nss] section of SSSD config /etc/sssd/sssd.conf. 180 300 600 900 1800 86400 300 SSSD ssh_known_hosts_timeout option Value of the ssh_known_hosts_timeout option in the [ssh] section of SSSD configuration file /etc/sssd/sssd.conf. 180 300 600 900 1800 86400 180 Install sssd-ipa Package The sssd-ipa package can be installed with the following command: $ sudo dnf install sssd-ipa SRG-OS-000480-GPOS-00227 sssd-ipa provides the IPA back end that the SSSD can utilize to fetch identity data from and authenticate against an IPA server. # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common; then if ! rpm -q --quiet "sssd-ipa" ; then dnf install -y "sssd-ipa" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_sssd-ipa_installed - name: Ensure sssd-ipa is installed ansible.builtin.package: name: sssd-ipa state: present when: '"sssd-common" in ansible_facts.packages' tags: - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_sssd-ipa_installed include install_sssd-ipa class install_sssd-ipa { package { 'sssd-ipa': ensure => 'installed', } } package --add=sssd-ipa [[packages]] name = "sssd-ipa" version = "*" package install sssd-ipa dnf install sssd-ipa Enable the SSSD Service The SSSD service should be enabled. The sssd service can be enabled with the following command: $ sudo systemctl enable sssd.service The service requires a valid sssd configuration. If the configuration is not present, the service will fail to start and consequently this rule will be reported as failing. The configuration shipped in your distribution package might not be sufficient. Manual modification of configuration files might be required. 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) IA-5(10) PR.AC-1 PR.AC-6 PR.AC-7 R67 # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common && { ( rpm --quiet -q sssd-common && rpm --quiet -q kernel ); }; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'sssd.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'sssd.service' fi "$SYSTEMCTL_EXEC" enable 'sssd.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(10) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_sssd_enabled - name: Enable the SSSD Service - Enable service sssd block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable the SSSD Service - Enable Service sssd ansible.builtin.systemd: name: sssd enabled: true state: started masked: false when: - '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(10) - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_sssd_enabled - special_service_block when: - '"sssd-common" in ansible_facts.packages' - ( "sssd-common" in ansible_facts.packages and "kernel" in ansible_facts.packages ) include enable_sssd class enable_sssd { service {'sssd': enable => true, ensure => 'running', } } [customizations.services] enabled = ["sssd"] service enable sssd Certificate status checking in SSSD Multifactor solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards. Configuring certificate_verification to ocsp_dgst= ensures that certificates for multifactor solutions are checked via Online Certificate Status Protocol (OCSP). IA-2(11) SRG-OS-000375-GPOS-00160 SRG-OS-000377-GPOS-00162 Ensuring that multifactor solutions certificates are checked via Online Certificate Status Protocol (OCSP) ensures the security of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common; then var_sssd_certificate_verification_digest_function='' # sssd configuration files must be created with 600 permissions if they don't exist # otherwise the sssd module fails to start OLD_UMASK=$(umask) umask u=rw,go= MAIN_CONF="/etc/sssd/conf.d/certificate_verification.conf" found=false # set value in all files if they contain section or key for f in $(echo -n "$MAIN_CONF /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[sssd\]([^\n\[]*\n+)+?[[:space:]]*certificate_verification" "$f"; then sed -i "s/certificate_verification[^(\n)]*/certificate_verification=ocsp_dgst=$var_sssd_certificate_verification_digest_function/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[sssd\]" "$f"; then sed -i "/[[:space:]]*\[sssd\]/a certificate_verification=ocsp_dgst=$var_sssd_certificate_verification_digest_function" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "$MAIN_CONF /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[sssd]\ncertificate_verification=ocsp_dgst=$var_sssd_certificate_verification_digest_function" >> "$file" fi umask $OLD_UMASK else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-IA-2(11) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_certificate_verification - name: XCCDF Value var_sssd_certificate_verification_digest_function # promote to variable set_fact: var_sssd_certificate_verification_digest_function: !!str tags: - always - name: Ensure that "certificate_verification" is not set in /etc/sssd/sssd.conf community.general.ini_file: path: /etc/sssd/sssd.conf section: sssd option: certificate_verification state: absent mode: 384 when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-IA-2(11) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_certificate_verification - name: Ensure that "certificate_verification" is not set in /etc/sssd/conf.d/*.conf community.general.ini_file: path: /etc/sssd/conf.d/*.conf section: sssd option: certificate_verification state: absent mode: 384 when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-IA-2(11) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_certificate_verification - name: Ensure that "certificate_verification" is set community.general.ini_file: path: /etc/sssd/conf.d/certificate_verification.conf section: sssd option: certificate_verification value: ocsp_dgst={{ var_sssd_certificate_verification_digest_function }} state: present mode: 384 when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-IA-2(11) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_certificate_verification Enable Certmap in SSSD SSSD should be configured to verify the certificate of the user or group. To set this up ensure that section like certmap/testing.test/rule_name is setup in /etc/sssd/sssd.conf. For example [certmap/testing.test/rule_name] matchrule =<SAN>.*EDIPI@mil maprule = (userCertificate;binary={cert!bin}) domains = testing.test Automatic remediation of this control is not available, since all of the settings in in the certmap need to be customized. IA-5 (2) (c) SRG-OS-000068-GPOS-00036 Without mapping the certificate used to authenticate to the user account, the ability to determine the identity of the individual user or group will not be available for forensic analysis. Enable Smartcards in SSSD SSSD should be configured to authenticate access to the system using smart cards. To enable smart cards in SSSD, set pam_cert_auth to True under the [pam] section in /etc/sssd/sssd.conf. For example: [pam] pam_cert_auth = True Add or update "pam_sss.so" line in auth section of "/etc/pam.d/system-auth" file to include "try_cert_auth" or "require_cert_auth" option, like in the following example: /etc/pam.d/system-auth:auth [success=done authinfo_unavail=ignore ignore=ignore default=die] pam_sss.so try_cert_auth Also add or update "pam_sss.so" line in auth section of "/etc/pam.d/smartcard-auth" file to include the "allow_missing_name" option, like in the following example: /etc/pam.d/smartcard-auth:auth sufficient pam_sss.so allow_missing_name 0421 0422 0431 0974 1173 1401 1504 1505 1546 1557 1558 1559 1560 1561 Req-8.3 SRG-OS-000375-GPOS-00160 SRG-OS-000105-GPOS-00052 SRG-OS-000106-GPOS-00053 SRG-OS-000107-GPOS-00054 SRG-OS-000108-GPOS-00055 Using an authentication device, such as a CAC or token that is separate from the information system, ensures that even if the information system is compromised, that compromise will not affect credentials stored on the authentication device. Multi-Factor Authentication (MFA) solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards or similar secure authentication devices issued by an organization or identity provider. # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common; then # sssd configuration files must be created with 600 permissions if they don't exist # otherwise the sssd module fails to start OLD_UMASK=$(umask) umask u=rw,go= found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[pam\]([^\n\[]*\n+)+?[[:space:]]*pam_cert_auth" "$f"; then sed -i "s/pam_cert_auth[^(\n)]*/pam_cert_auth=True/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[pam\]" "$f"; then sed -i "/[[:space:]]*\[pam\]/a pam_cert_auth=True" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[pam]\npam_cert_auth=True" >> "$file" fi umask $OLD_UMASK if [ -f /usr/bin/authselect ]; then if ! authselect check; then echo " authselect integrity check failed. Remediation aborted! This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. It is not recommended to manually edit the PAM files when authselect tool is available. In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended." exit 1 fi authselect enable-feature with-smartcard authselect apply-changes -b else if ! grep -qP "^\s*auth\s+sufficient\s+pam_sss.so\s*.*" "/etc/pam.d/smartcard-auth"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*auth\s+.*\s+pam_sss.so\s*' "/etc/pam.d/smartcard-auth")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*auth\s+).*(\bpam_sss.so.*)/\1sufficient \2/" "/etc/pam.d/smartcard-auth" else echo "auth sufficient pam_sss.so" >> "/etc/pam.d/smartcard-auth" fi fi # Check the option if ! grep -qP "^\s*auth\s+sufficient\s+pam_sss.so\s*.*\sallow_missing_name\b" "/etc/pam.d/smartcard-auth"; then sed -i -E --follow-symlinks "/\s*auth\s+sufficient\s+pam_sss.so.*/ s/$/ allow_missing_name/" "/etc/pam.d/smartcard-auth" fi if ! grep -qP "^\s*auth\s+\[success=done authinfo_unavail=ignore ignore=ignore default=die\]\s+pam_sss.so\s*.*" "/etc/pam.d/system-auth"; then # Line matching group + control + module was not found. Check group + module. if [ "$(grep -cP '^\s*auth\s+.*\s+pam_sss.so\s*' "/etc/pam.d/system-auth")" -eq 1 ]; then # The control is updated only if one single line matches. sed -i -E --follow-symlinks "s/^(\s*auth\s+).*(\bpam_sss.so.*)/\1[success=done authinfo_unavail=ignore ignore=ignore default=die] \2/" "/etc/pam.d/system-auth" else echo "auth [success=done authinfo_unavail=ignore ignore=ignore default=die] pam_sss.so" >> "/etc/pam.d/system-auth" fi fi # Check the option if ! grep -qP "^\s*auth\s+\[success=done authinfo_unavail=ignore ignore=ignore default=die\]\s+pam_sss.so\s*.*\stry_cert_auth\b" "/etc/pam.d/system-auth"; then sed -i -E --follow-symlinks "/\s*auth\s+\[success=done authinfo_unavail=ignore ignore=ignore default=die\]\s+pam_sss.so.*/ s/$/ try_cert_auth/" "/etc/pam.d/system-auth" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Test for domain group ansible.builtin.command: grep '^\s*\[domain\/[^]]*]' /etc/sssd/sssd.conf register: test_grep_domain failed_when: false changed_when: false check_mode: false when: '"sssd-common" in ansible_facts.packages' tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Add default domain group (if no domain there) community.general.ini_file: path: /etc/sssd/sssd.conf section: '{{ item.section }}' option: '{{ item.option }}' value: '{{ item.value }}' create: true mode: 384 with_items: - section: sssd option: domains value: default - section: domain/default option: id_provider value: files when: - '"sssd-common" in ansible_facts.packages' - test_grep_domain.stdout is defined - test_grep_domain.stdout | length < 1 tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Enable Smartcards in SSSD community.general.ini_file: dest: /etc/sssd/sssd.conf section: pam option: pam_cert_auth value: 'True' create: true mode: 384 when: '"sssd-common" in ansible_facts.packages' tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Find all the conf files inside /etc/sssd/conf.d/ ansible.builtin.find: paths: /etc/sssd/conf.d/ patterns: '*.conf' register: sssd_conf_d_files when: '"sssd-common" in ansible_facts.packages' tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Fix pam_cert_auth configuration in /etc/sssd/conf.d/ ansible.builtin.replace: path: '{{ item.path }}' regexp: '[^#]*pam_cert_auth.*' replace: pam_cert_auth = True with_items: '{{ sssd_conf_d_files.files }}' when: '"sssd-common" in ansible_facts.packages' tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Enable Smartcards in SSSD - Check if system relies on authselect ansible.builtin.stat: path: /usr/bin/authselect register: result_authselect_present when: '"sssd-common" in ansible_facts.packages' tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Enable Smartcards in SSSD - Remediate using authselect block: - name: Enable Smartcards in SSSD - Check integrity of authselect current profile ansible.builtin.command: cmd: authselect check register: result_authselect_check_cmd changed_when: false check_mode: false failed_when: false - name: Enable Smartcards in SSSD - Informative message based on the authselect integrity check result ansible.builtin.assert: that: - ansible_check_mode or result_authselect_check_cmd.rc == 0 fail_msg: - authselect integrity check failed. Remediation aborted! - This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact. - It is not recommended to manually edit the PAM files when authselect tool is available. - In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended. success_msg: - authselect integrity check passed - name: Enable Smartcards in SSSD - Get authselect current features ansible.builtin.shell: cmd: authselect current | tail -n+3 | awk '{ print $2 }' register: result_authselect_features changed_when: false check_mode: false when: - result_authselect_check_cmd is success - name: Enable Smartcards in SSSD - Ensure "with-smartcard" feature is enabled using authselect tool ansible.builtin.command: cmd: authselect enable-feature with-smartcard register: result_authselect_enable_feature_cmd when: - result_authselect_check_cmd is success - result_authselect_features.stdout is not search("with-smartcard") - name: Enable Smartcards in SSSD - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_enable_feature_cmd is not skipped - result_authselect_enable_feature_cmd is success when: - '"sssd-common" in ansible_facts.packages' - result_authselect_present.stat.exists tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards - name: Enable Smartcards in SSSD - Remediate by directly editing PAM files block: - name: Enable Smartcards in SSSD - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Enable Smartcards in SSSD - Check if expected PAM module line is present in /etc/pam.d/smartcard-auth ansible.builtin.lineinfile: path: /etc/pam.d/smartcard-auth regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_sss.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Enable Smartcards in SSSD - Include or update the PAM module line in /etc/pam.d/smartcard-auth block: - name: Enable Smartcards in SSSD - Check if required PAM module line is present in /etc/pam.d/smartcard-auth with different control ansible.builtin.lineinfile: path: /etc/pam.d/smartcard-auth regexp: ^\s*auth\s+.*\s+pam_sss.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Enable Smartcards in SSSD - Ensure the correct control for the required PAM module line in /etc/pam.d/smartcard-auth ansible.builtin.replace: dest: /etc/pam.d/smartcard-auth regexp: ^(\s*auth\s+).*(\bpam_sss.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Enable Smartcards in SSSD - Ensure the required PAM module line is included in /etc/pam.d/smartcard-auth ansible.builtin.lineinfile: dest: /etc/pam.d/smartcard-auth line: auth {{ pam_module_control }} pam_sss.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Enable Smartcards in SSSD - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Enable Smartcards in SSSD - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: sufficient - name: Enable Smartcards in SSSD - Check if the required PAM module option is present in /etc/pam.d/smartcard-auth ansible.builtin.lineinfile: path: /etc/pam.d/smartcard-auth regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_sss.so\s*.*\sallow_missing_name\b state: absent check_mode: true changed_when: false register: result_pam_module_sssd_enable_smartcards_option_present - name: Enable Smartcards in SSSD - Ensure the "allow_missing_name" PAM option for "pam_sss.so" is included in /etc/pam.d/smartcard-auth ansible.builtin.lineinfile: path: /etc/pam.d/smartcard-auth backrefs: true regexp: ^(\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_sss.so.*) line: \1 allow_missing_name state: present register: result_pam_sssd_enable_smartcards_add when: - result_pam_module_sssd_enable_smartcards_option_present.found is defined - result_pam_module_sssd_enable_smartcards_option_present.found == 0 - name: Enable Smartcards in SSSD - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '[success=done authinfo_unavail=ignore ignore=ignore default=die]' - name: Enable Smartcards in SSSD - Check if expected PAM module line is present in /etc/pam.d/system-auth ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_sss.so\s*.* state: absent check_mode: true changed_when: false register: result_pam_line_present - name: Enable Smartcards in SSSD - Include or update the PAM module line in /etc/pam.d/system-auth block: - name: Enable Smartcards in SSSD - Check if required PAM module line is present in /etc/pam.d/system-auth with different control ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: ^\s*auth\s+.*\s+pam_sss.so\s* state: absent check_mode: true changed_when: false register: result_pam_line_other_control_present - name: Enable Smartcards in SSSD - Ensure the correct control for the required PAM module line in /etc/pam.d/system-auth ansible.builtin.replace: dest: /etc/pam.d/system-auth regexp: ^(\s*auth\s+).*(\bpam_sss.so.*) replace: \1{{ pam_module_control }} \2 register: result_pam_module_edit when: - result_pam_line_other_control_present.found == 1 - name: Enable Smartcards in SSSD - Ensure the required PAM module line is included in /etc/pam.d/system-auth ansible.builtin.lineinfile: dest: /etc/pam.d/system-auth line: auth {{ pam_module_control }} pam_sss.so register: result_pam_module_add when: - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found > 1 - name: Enable Smartcards in SSSD - Ensure authselect changes are applied ansible.builtin.command: cmd: authselect apply-changes -b when: - result_authselect_present is defined - result_authselect_present.stat.exists - |- (result_pam_module_add is defined and result_pam_module_add.changed) or (result_pam_module_edit is defined and result_pam_module_edit.changed) when: - result_pam_line_present.found is defined - result_pam_line_present.found == 0 - name: Enable Smartcards in SSSD - Define a fact for control already filtered in case filters are used ansible.builtin.set_fact: pam_module_control: '[success=done authinfo_unavail=ignore ignore=ignore default=die]' - name: Enable Smartcards in SSSD - Check if the required PAM module option is present in /etc/pam.d/system-auth ansible.builtin.lineinfile: path: /etc/pam.d/system-auth regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_sss.so\s*.*\stry_cert_auth\b state: absent check_mode: true changed_when: false register: result_pam_module_sssd_enable_smartcards_option_present - name: Enable Smartcards in SSSD - Ensure the "try_cert_auth" PAM option for "pam_sss.so" is included in /etc/pam.d/system-auth ansible.builtin.lineinfile: path: /etc/pam.d/system-auth backrefs: true regexp: ^(\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_sss.so.*) line: \1 try_cert_auth state: present register: result_pam_sssd_enable_smartcards_add when: - result_pam_module_sssd_enable_smartcards_option_present.found is defined - result_pam_module_sssd_enable_smartcards_option_present.found == 0 when: - '"sssd-common" in ansible_facts.packages' - not result_authselect_present.stat.exists tags: - PCI-DSS-Req-8.3 - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_enable_smartcards Configure SSSD's Memory Cache to Expire SSSD's memory cache should be configured to set to expire records after seconds. To configure SSSD to expire memory cache, set memcache_timeout to under the [nss] section in /etc/sssd/sssd.conf. For example: [nss] memcache_timeout = 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) IA-5(13) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000383-GPOS-00166 If cached authentication information is out-of-date, the validity of the authentication information may be questionable. # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common; then var_sssd_memcache_timeout='' # sssd configuration files must be created with 600 permissions if they don't exist # otherwise the sssd module fails to start OLD_UMASK=$(umask) umask u=rw,go= found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/sssd/sssd.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[nss\]([^\n\[]*\n+)+?[[:space:]]*memcache_timeout" "$f"; then sed -i "s/memcache_timeout[^(\n)]*/memcache_timeout=$var_sssd_memcache_timeout/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[nss\]" "$f"; then sed -i "/[[:space:]]*\[nss\]/a memcache_timeout=$var_sssd_memcache_timeout" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/sssd/sssd.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[nss]\nmemcache_timeout=$var_sssd_memcache_timeout" >> "$file" fi umask $OLD_UMASK else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_memcache_timeout - unknown_strategy - name: XCCDF Value var_sssd_memcache_timeout # promote to variable set_fact: var_sssd_memcache_timeout: !!str tags: - always - name: Test for domain group ansible.builtin.command: grep '\s*\[domain\/[^]]*]' /etc/sssd/sssd.conf register: test_grep_domain failed_when: false changed_when: false check_mode: false when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_memcache_timeout - unknown_strategy - name: Add default domain group (if no domain there) community.general.ini_file: path: /etc/sssd/sssd.conf section: '{{ item.section }}' option: '{{ item.option }}' value: '{{ item.value }}' create: true mode: 384 with_items: - section: sssd option: domains value: default - section: domain/default option: id_provider value: files when: - '"sssd-common" in ansible_facts.packages' - test_grep_domain.stdout is defined - test_grep_domain.stdout | length < 1 tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_memcache_timeout - unknown_strategy - name: Configure SSSD's Memory Cache to Expire community.general.ini_file: dest: /etc/sssd/sssd.conf section: nss option: memcache_timeout value: '{{ var_sssd_memcache_timeout }}' create: true mode: 384 when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_memcache_timeout - unknown_strategy Configure SSSD to Expire Offline Credentials SSSD should be configured to expire offline credentials after 1 day. To configure SSSD to expire offline credentials, set offline_credentials_expiration to 1 under the [pam] section in /etc/sssd/sssd.conf. For example: [pam] offline_credentials_expiration = 1 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) IA-5(13) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000383-GPOS-00166 If cached authentication information is out-of-date, the validity of the authentication information may be questionable. # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common; then # sssd configuration files must be created with 600 permissions if they don't exist # otherwise the sssd module fails to start OLD_UMASK=$(umask) umask u=rw,go= found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[pam\]([^\n\[]*\n+)+?[[:space:]]*offline_credentials_expiration" "$f"; then sed -i "s/offline_credentials_expiration[^(\n)]*/offline_credentials_expiration=1/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[pam\]" "$f"; then sed -i "/[[:space:]]*\[pam\]/a offline_credentials_expiration=1" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[pam]\noffline_credentials_expiration=1" >> "$file" fi umask $OLD_UMASK else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_offline_cred_expiration - name: Test for domain group ansible.builtin.command: grep '\s*\[domain\/[^]]*]' /etc/sssd/sssd.conf register: test_grep_domain failed_when: false changed_when: false check_mode: false when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_offline_cred_expiration - name: Add default domain group (if no domain there) community.general.ini_file: path: /etc/sssd/sssd.conf section: '{{ item.section }}' option: '{{ item.option }}' value: '{{ item.value }}' create: true mode: 384 with_items: - section: sssd option: domains value: default - section: domain/default option: id_provider value: files when: - '"sssd-common" in ansible_facts.packages' - test_grep_domain.stdout is defined - test_grep_domain.stdout | length < 1 tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_offline_cred_expiration - name: Configure SSD to Expire Offline Credentials community.general.ini_file: dest: /etc/sssd/sssd.conf section: pam option: offline_credentials_expiration value: 1 create: true mode: 384 when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_offline_cred_expiration - name: Find all the conf files inside /etc/sssd/conf.d/ ansible.builtin.find: paths: /etc/sssd/conf.d/ patterns: '*.conf' register: sssd_conf_d_files when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_offline_cred_expiration - name: Fix offline_credentials_expiration configuration in /etc/sssd/conf.d/ ansible.builtin.replace: path: '{{ item.path }}' regexp: '[^#]*offline_credentials_expiration.*' replace: offline_credentials_expiration = 1 with_items: '{{ sssd_conf_d_files.files }}' when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - configure_strategy - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_offline_cred_expiration Configure SSSD to run as user sssd SSSD processes should be configured to run as user sssd, not root. SRG-OS-000480-GPOS-00227 To minimize privileges of SSSD processes, they are configured to run as non-root user. # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common; then MAIN_CONF="/etc/sssd/conf.d/ospp.conf" # sssd configuration files must be created with 600 permissions if they don't exist # otherwise the sssd module fails to start OLD_UMASK=$(umask) umask u=rw,go= found=false # set value in all files if they contain section or key for f in $(echo -n "$MAIN_CONF /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[sssd\]([^\n\[]*\n+)+?[[:space:]]*user" "$f"; then sed -i "s/user[^(\n)]*/user=sssd/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[sssd\]" "$f"; then sed -i "/[[:space:]]*\[sssd\]/a user=sssd" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "$MAIN_CONF /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[sssd]\nuser=sssd" >> "$file" fi umask $OLD_UMASK else >&2 echo 'Remediation is not applicable, nothing was done' fi Configure SSSD to Expire SSH Known Hosts SSSD should be configured to expire keys from known SSH hosts after seconds. To configure SSSD to known SSH hosts, set ssh_known_hosts_timeout to under the [ssh] section in /etc/sssd/sssd.conf. For example: [ssh] ssh_known_hosts_timeout = 1 12 15 16 5 DSS05.04 DSS05.05 DSS05.07 DSS05.10 DSS06.03 DSS06.10 4.3.3.2.2 4.3.3.5.1 4.3.3.5.2 4.3.3.6.1 4.3.3.6.2 4.3.3.6.3 4.3.3.6.4 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.3.6.9 4.3.3.7.2 4.3.3.7.4 SR 1.1 SR 1.10 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 A.18.1.4 A.7.1.1 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.2 A.9.4.3 CM-6(a) IA-5(13) PR.AC-1 PR.AC-6 PR.AC-7 SRG-OS-000383-GPOS-00166 If cached authentication information is out-of-date, the validity of the authentication information may be questionable. # Remediation is applicable only in certain platforms if rpm --quiet -q sssd-common; then var_sssd_ssh_known_hosts_timeout='' # sssd configuration files must be created with 600 permissions if they don't exist # otherwise the sssd module fails to start OLD_UMASK=$(umask) umask u=rw,go= found=false # set value in all files if they contain section or key for f in $(echo -n "/etc/sssd/sssd.conf"); do if [ ! -e "$f" ]; then continue fi # find key in section and change value if grep -qzosP "[[:space:]]*\[ssh\]([^\n\[]*\n+)+?[[:space:]]*ssh_known_hosts_timeout" "$f"; then sed -i "s/ssh_known_hosts_timeout[^(\n)]*/ssh_known_hosts_timeout=$var_sssd_ssh_known_hosts_timeout/" "$f" found=true # find section and add key = value to it elif grep -qs "[[:space:]]*\[ssh\]" "$f"; then sed -i "/[[:space:]]*\[ssh\]/a ssh_known_hosts_timeout=$var_sssd_ssh_known_hosts_timeout" "$f" found=true fi done # if section not in any file, append section with key = value to FIRST file in files parameter if ! $found ; then file=$(echo "/etc/sssd/sssd.conf" | cut -f1 -d ' ') mkdir -p "$(dirname "$file")" echo -e "[ssh]\nssh_known_hosts_timeout=$var_sssd_ssh_known_hosts_timeout" >> "$file" fi umask $OLD_UMASK else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_ssh_known_hosts_timeout - unknown_strategy - name: XCCDF Value var_sssd_ssh_known_hosts_timeout # promote to variable set_fact: var_sssd_ssh_known_hosts_timeout: !!str tags: - always - name: Test for domain group ansible.builtin.command: grep '\s*\[domain\/[^]]*]' /etc/sssd/sssd.conf register: test_grep_domain failed_when: false changed_when: false check_mode: false when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_ssh_known_hosts_timeout - unknown_strategy - name: Add default domain group (if no domain there) community.general.ini_file: path: /etc/sssd/sssd.conf section: '{{ item.section }}' option: '{{ item.option }}' value: '{{ item.value }}' create: true mode: 384 with_items: - section: sssd option: domains value: default - section: domain/default option: id_provider value: files when: - '"sssd-common" in ansible_facts.packages' - test_grep_domain.stdout is defined - test_grep_domain.stdout | length < 1 tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_ssh_known_hosts_timeout - unknown_strategy - name: Configure SSSD to Expire SSH Known Hosts community.general.ini_file: dest: /etc/sssd/sssd.conf section: ssh option: ssh_known_hosts_timeout value: '{{ var_sssd_ssh_known_hosts_timeout }}' create: true mode: 384 when: '"sssd-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(13) - low_complexity - medium_disruption - medium_severity - no_reboot_needed - sssd_ssh_known_hosts_timeout - unknown_strategy USBGuard daemon The USBGuard daemon enforces the USB device authorization policy for all USB devices. Install usbguard Package The usbguard package can be installed with the following command: $ sudo dnf install usbguard 1418 CM-8(3) IA-3 FMT_SMF_EXT.1 SRG-OS-000378-GPOS-00163 SRG-APP-000141-CTR-000315 usbguard is a software framework that helps to protect against rogue USB devices by implementing basic whitelisting/blacklisting capabilities based on USB device attributes. # Remediation is applicable only in certain platforms if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel ); then if ! rpm -q --quiet "usbguard" ; then dnf install -y "usbguard" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-8(3) - NIST-800-53-IA-3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_usbguard_installed - name: Ensure usbguard is installed ansible.builtin.package: name: usbguard state: present when: ( ansible_architecture != "s390x" and "kernel" in ansible_facts.packages ) tags: - NIST-800-53-CM-8(3) - NIST-800-53-IA-3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_usbguard_installed include install_usbguard class install_usbguard { package { 'usbguard': ensure => 'installed', } } package --add=usbguard --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 extensions: - usbguard [[packages]] name = "usbguard" version = "*" package install usbguard dnf install usbguard Enable the USBGuard Service The USBGuard service should be enabled. The usbguard service can be enabled with the following command: $ sudo systemctl enable usbguard.service 1418 CM-8(3)(a) IA-3 FMT_SMF_EXT.1 SRG-OS-000378-GPOS-00163 SRG-APP-000141-CTR-000315 The usbguard service must be running in order to enforce the USB device authorization policy for all USB devices. # Remediation is applicable only in certain platforms if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel ); then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'usbguard.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'usbguard.service' fi "$SYSTEMCTL_EXEC" enable 'usbguard.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-8(3)(a) - NIST-800-53-IA-3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_usbguard_enabled - name: Enable the USBGuard Service - Enable service usbguard block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable the USBGuard Service - Enable Service usbguard ansible.builtin.systemd: name: usbguard enabled: true state: started masked: false when: - '"usbguard" in ansible_facts.packages' tags: - NIST-800-53-CM-8(3)(a) - NIST-800-53-IA-3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_usbguard_enabled - special_service_block when: ( ansible_architecture != "s390x" and "kernel" in ansible_facts.packages ) include enable_usbguard class enable_usbguard { service {'usbguard': enable => true, ensure => 'running', } } --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig metadata: annotations: complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed spec: config: ignition: version: 3.1.0 systemd: units: - name: usbguard.service enabled: true [customizations.services] enabled = ["usbguard"] service enable usbguard Log USBGuard daemon audit events using Linux Audit To configure USBGuard daemon to log via Linux Audit (as opposed directly to a file), AuditBackend option in /etc/usbguard/usbguard-daemon.conf needs to be set to LinuxAudit. AU-2 CM-8(3) IA-3 FMT_SMF_EXT.1 SRG-OS-000062-GPOS-00031 SRG-OS-000471-GPOS-00215 SRG-APP-000141-CTR-000315 Using the Linux Audit logging allows for centralized trace of events. # Remediation is applicable only in certain platforms if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel ) && { rpm --quiet -q usbguard; }; then if [ -e "/etc/usbguard/usbguard-daemon.conf" ] ; then LC_ALL=C sed -i "/^[ \\t]*AuditBackend=/Id" "/etc/usbguard/usbguard-daemon.conf" else touch "/etc/usbguard/usbguard-daemon.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/usbguard/usbguard-daemon.conf" cp "/etc/usbguard/usbguard-daemon.conf" "/etc/usbguard/usbguard-daemon.conf.bak" # Insert at the end of the file printf '%s\n' "AuditBackend=LinuxAudit" >> "/etc/usbguard/usbguard-daemon.conf" # Clean up after ourselves. rm "/etc/usbguard/usbguard-daemon.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-2 - NIST-800-53-CM-8(3) - NIST-800-53-IA-3 - configure_strategy - configure_usbguard_auditbackend - low_complexity - low_disruption - low_severity - no_reboot_needed - name: Log USBGuard daemon audit events using Linux Audit block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/usbguard/usbguard-daemon.conf create: true regexp: (?i)^[ \\t]*AuditBackend= state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/usbguard/usbguard-daemon.conf ansible.builtin.lineinfile: path: /etc/usbguard/usbguard-daemon.conf create: true regexp: (?i)^[ \\t]*AuditBackend= state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/usbguard/usbguard-daemon.conf ansible.builtin.lineinfile: path: /etc/usbguard/usbguard-daemon.conf create: true regexp: (?i)^[ \\t]*AuditBackend= line: AuditBackend=LinuxAudit state: present when: - ( ansible_architecture != "s390x" and "kernel" in ansible_facts.packages ) - '"usbguard" in ansible_facts.packages' tags: - NIST-800-53-AU-2 - NIST-800-53-CM-8(3) - NIST-800-53-IA-3 - configure_strategy - configure_usbguard_auditbackend - low_complexity - low_disruption - low_severity - no_reboot_needed --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig metadata: annotations: complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed complianceascode.io/ocp-version: '>=4.7.0' spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %0A%23%0A%23%20Rule%20set%20file%20path.%0A%23%0A%23%20The%20USBGuard%20daemon%20will%20use%20this%20file%20to%20load%20the%20policy%0A%23%20rule%20set%20from%20it%20and%20to%20write%20new%20rules%20received%20via%20the%0A%23%20IPC%20interface.%0A%23%0A%23%20RuleFile%3D/path/to/rules.conf%0A%23%0ARuleFile%3D/etc/usbguard/rules.conf%0A%0A%23%0A%23%20Rule%20set%20folder%20path.%0A%23%0A%23%20The%20USBGuard%20daemon%20will%20use%20this%20folder%20to%20load%20the%20policy%0A%23%20rule%20set%20from%20it%20and%20to%20write%20new%20rules%20received%20via%20the%0A%23%20IPC%20interface.%20Usually%2C%20we%20set%20the%20option%20to%0A%23%20/etc/usbguard/rules.d/.%20The%20USBGuard%20daemon%20is%20supposed%20to%0A%23%20behave%20like%20any%20other%20standard%20Linux%20daemon%20therefore%20it%0A%23%20loads%20rule%20files%20in%20alpha-numeric%20order.%20File%20names%20inside%0A%23%20RuleFolder%20directory%20should%20start%20with%20a%20two-digit%20number%0A%23%20prefix%20indicating%20the%20position%2C%20in%20which%20the%20rules%20are%0A%23%20scanned%20by%20the%20daemon.%0A%23%0A%23%20RuleFolder%3D/path/to/rulesfolder/%0A%23%0ARuleFolder%3D/etc/usbguard/rules.d/%0A%0A%23%0A%23%20Implicit%20policy%20target.%0A%23%0A%23%20How%20to%20treat%20devices%20that%20don%27t%20match%20any%20rule%20in%20the%0A%23%20policy.%20One%20of%3A%0A%23%0A%23%20%2A%20allow%20%20-%20authorize%20the%20device%0A%23%20%2A%20block%20%20-%20block%20the%20device%0A%23%20%2A%20reject%20-%20remove%20the%20device%0A%23%0AImplicitPolicyTarget%3Dblock%0A%0A%23%0A%23%20Present%20device%20policy.%0A%23%0A%23%20How%20to%20treat%20devices%20that%20are%20already%20connected%20when%20the%0A%23%20daemon%20starts.%20One%20of%3A%0A%23%0A%23%20%2A%20allow%20%20%20%20%20%20%20%20-%20authorize%20every%20present%20device%0A%23%20%2A%20block%20%20%20%20%20%20%20%20-%20deauthorize%20every%20present%20device%0A%23%20%2A%20reject%20%20%20%20%20%20%20-%20remove%20every%20present%20device%0A%23%20%2A%20keep%20%20%20%20%20%20%20%20%20-%20just%20sync%20the%20internal%20state%20and%20leave%20it%0A%23%20%2A%20apply-policy%20-%20evaluate%20the%20ruleset%20for%20every%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20device%0A%23%0APresentDevicePolicy%3Dapply-policy%0A%0A%23%0A%23%20Present%20controller%20policy.%0A%23%0A%23%20How%20to%20treat%20USB%20controllers%20that%20are%20already%20connected%0A%23%20when%20the%20daemon%20starts.%20One%20of%3A%0A%23%0A%23%20%2A%20allow%20%20%20%20%20%20%20%20-%20authorize%20every%20present%20device%0A%23%20%2A%20block%20%20%20%20%20%20%20%20-%20deauthorize%20every%20present%20device%0A%23%20%2A%20reject%20%20%20%20%20%20%20-%20remove%20every%20present%20device%0A%23%20%2A%20keep%20%20%20%20%20%20%20%20%20-%20just%20sync%20the%20internal%20state%20and%20leave%20it%0A%23%20%2A%20apply-policy%20-%20evaluate%20the%20ruleset%20for%20every%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20device%0A%23%0APresentControllerPolicy%3Dkeep%0A%0A%23%0A%23%20Inserted%20device%20policy.%0A%23%0A%23%20How%20to%20treat%20USB%20devices%20that%20are%20already%20connected%0A%23%20%2Aafter%2A%20the%20daemon%20starts.%20One%20of%3A%0A%23%0A%23%20%2A%20block%20%20%20%20%20%20%20%20-%20deauthorize%20every%20present%20device%0A%23%20%2A%20reject%20%20%20%20%20%20%20-%20remove%20every%20present%20device%0A%23%20%2A%20apply-policy%20-%20evaluate%20the%20ruleset%20for%20every%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20device%0A%23%0AInsertedDevicePolicy%3Dapply-policy%0A%0A%23%0A%23%20Control%20which%20devices%20are%20authorized%20by%20default.%0A%23%0A%23%20The%20USBGuard%20daemon%20modifies%20some%20the%20default%20authorization%20state%20attributes%0A%23%20of%20controller%20devices.%20This%20setting%2C%20enables%20you%20to%20define%20what%20value%20the%0A%23%20default%20authorization%20is%20set%20to.%0A%23%0A%23%20%2A%20keep%20%20%20%20%20%20%20%20%20-%20do%20not%20change%20the%20authorization%20state%0A%23%20%2A%20none%20%20%20%20%20%20%20%20%20-%20every%20new%20device%20starts%20out%20deauthorized%0A%23%20%2A%20all%20%20%20%20%20%20%20%20%20%20-%20every%20new%20device%20starts%20out%20authorized%0A%23%20%2A%20internal%20%20%20%20%20-%20internal%20devices%20start%20out%20authorized%2C%20external%20devices%20start%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20out%20deauthorized%20%28this%20requires%20the%20ACPI%20tables%20to%20properly%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%20internal%20devices%2C%20and%20kernel%20support%29%0A%23%0A%23AuthorizedDefault%3Dnone%0A%0A%23%0A%23%20Restore%20controller%20device%20state.%0A%23%0A%23%20The%20USBGuard%20daemon%20modifies%20some%20attributes%20of%20controller%0A%23%20devices%20like%20the%20default%20authorization%20state%20of%20new%20child%20device%0A%23%20instances.%20Using%20this%20setting%2C%20you%20can%20control%20whether%20the%0A%23%20daemon%20will%20try%20to%20restore%20the%20attribute%20values%20to%20the%20state%0A%23%20before%20modification%20on%20shutdown.%0A%23%0A%23%20SECURITY%20CONSIDERATIONS%3A%20If%20set%20to%20true%2C%20the%20USB%20authorization%0A%23%20policy%20could%20be%20bypassed%20by%20performing%20some%20sort%20of%20attack%20on%20the%0A%23%20daemon%20%28via%20a%20local%20exploit%20or%20via%20a%20USB%20device%29%20to%20make%20it%20shutdown%0A%23%20and%20restore%20to%20the%20operating-system%20default%20state%20%28known%20to%20be%20permissive%29.%0A%23%0ARestoreControllerDeviceState%3Dfalse%0A%0A%23%0A%23%20Device%20manager%20backend%0A%23%0A%23%20Which%20device%20manager%20backend%20implementation%20to%20use.%20One%20of%3A%0A%23%0A%23%20%2A%20uevent%20%20%20-%20Netlink%20based%20implementation%20which%20uses%20sysfs%20to%20scan%20for%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20devices%20and%20an%20uevent%20netlink%20socket%20for%20receiving%20USB%20device%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20related%20events.%0A%23%20%2A%20umockdev%20-%20umockdev%20based%20device%20manager%20capable%20of%20simulating%20devices%20based%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20on%20umockdev-record%20files.%20Useful%20for%20testing.%0A%23%0ADeviceManagerBackend%3Duevent%0A%0A%23%21%21%21%20WARNING%3A%20It%27s%20good%20practice%20to%20set%20at%20least%20one%20of%20the%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20two%20options%20bellow.%20If%20none%20of%20them%20are%20set%2C%20%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20the%20daemon%20will%20accept%20IPC%20connections%20from%20%20%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20anyone%2C%20thus%20allowing%20anyone%20to%20modify%20the%20%20%20%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20rule%20set%20and%20%28de%29authorize%20USB%20devices.%20%20%20%20%20%20%20%21%21%21%0A%0A%23%0A%23%20Users%20allowed%20to%20use%20the%20IPC%20interface.%0A%23%0A%23%20A%20space%20delimited%20list%20of%20usernames%20that%20the%20daemon%20will%0A%23%20accept%20IPC%20connections%20from.%0A%23%0A%23%20IPCAllowedUsers%3Dusername1%20username2%20...%0A%23%0AIPCAllowedUsers%3Droot%0A%0A%23%0A%23%20Groups%20allowed%20to%20use%20the%20IPC%20interface.%0A%23%0A%23%20A%20space%20delimited%20list%20of%20groupnames%20that%20the%20daemon%20will%0A%23%20accept%20IPC%20connections%20from.%0A%23%0A%23%20IPCAllowedGroups%3Dgroupname1%20groupname2%20...%0A%23%0AIPCAllowedGroups%3Dwheel%0A%0A%23%0A%23%20IPC%20access%20control%20definition%20files%20path.%0A%23%0A%23%20The%20files%20at%20this%20location%20will%20be%20interpreted%20by%20the%20daemon%0A%23%20as%20access%20control%20definition%20files.%20The%20%28base%29name%20of%20a%20file%0A%23%20should%20be%20in%20the%20form%3A%0A%23%0A%23%20%20%20%5Buser%5D%5B%3A%3Cgroup%3E%5D%0A%23%0A%23%20and%20should%20contain%20lines%20in%20the%20form%3A%0A%23%0A%23%20%20%20%3Csection%3E%3D%5Bprivilege%5D%20...%0A%23%0A%23%20This%20way%20each%20file%20defines%20who%20is%20able%20to%20connect%20to%20the%20IPC%0A%23%20bus%20and%20what%20privileges%20he%20has.%0A%23%0AIPCAccessControlFiles%3D/etc/usbguard/IPCAccessControl.d/%0A%0A%23%0A%23%20Generate%20device%20specific%20rules%20including%20the%20%22via-port%22%0A%23%20attribute.%0A%23%0A%23%20This%20option%20modifies%20the%20behavior%20of%20the%20allowDevice%0A%23%20action.%20When%20instructed%20to%20generate%20a%20permanent%20rule%2C%0A%23%20the%20action%20can%20generate%20a%20port%20specific%20rule.%20Because%0A%23%20some%20systems%20have%20unstable%20port%20numbering%2C%20the%20generated%0A%23%20rule%20might%20not%20match%20the%20device%20after%20rebooting%20the%20system.%0A%23%0A%23%20If%20set%20to%20false%2C%20the%20generated%20rule%20will%20still%20contain%0A%23%20the%20%22parent-hash%22%20attribute%20which%20also%20defines%20an%20association%0A%23%20to%20the%20parent%20device.%20See%20usbguard-rules.conf%285%29%20for%20more%0A%23%20details.%0A%23%0ADeviceRulesWithPort%3Dfalse%0A%0A%23%0A%23%20USBGuard%20Audit%20events%20log%20backend%0A%23%0A%23%20One%20of%3A%0A%23%0A%23%20%2A%20FileAudit%20-%20Log%20audit%20events%20into%20a%20file%20specified%20by%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20AuditFilePath%20setting%20%28see%20below%29%0A%23%20%2A%20LinuxAudit%20-%20Log%20audit%20events%20using%20the%20Linux%20Audit%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20subsystem%20%28using%20audit_log_user_message%29%0A%23%0AAuditBackend%3DLinuxAudit%0A%0A%23%0A%23%20USBGuard%20audit%20events%20log%20file%20path.%0A%23%0A%23AuditFilePath%3D/var/log/usbguard/usbguard-audit.log%0A%0A%23%0A%23%20Hides%20personally%20identifiable%20information%20such%20as%20device%20serial%20numbers%20and%0A%23%20hashes%20of%20descriptors%20%28which%20include%20the%20serial%20number%29%20from%20audit%20entries.%0A%23%0A%23HidePII%3Dfalse }} mode: 0600 path: /etc/usbguard/usbguard-daemon.conf overwrite: true Authorize Human Interface Devices in USBGuard daemon To allow authorization of Human Interface Devices (keyboard, mouse) by USBGuard daemon, add the line allow with-interface match-all { 03:*:* } to /etc/usbguard/rules.conf. This rule should be understood primarily as a convenience administration feature. This rule ensures that if the USBGuard default rules.conf file is present, it will alter it so that USB human interface devices are allowed. However, if the rules.conf file is altered by system administrator, the rule does not check if USB human interface devices are allowed. This assumes that an administrator modified the file with some purpose in mind. SRG-OS-000114-GPOS-00059 Without allowing Human Interface Devices, it might not be possible to interact with the system. # Remediation is applicable only in certain platforms if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel ); then # path of file with Usbguard rules rulesfile="/etc/usbguard/rules.conf" echo "allow with-interface match-all { 03:*:* }" >> $rulesfile else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - usbguard_allow_hid - name: Allow HID devices ansible.builtin.lineinfile: path: /etc/usbguard/rules.conf create: true regexp: '' line: allow with-interface match-all { 03:*:* } state: present when: ( ansible_architecture != "s390x" and "kernel" in ansible_facts.packages ) tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - usbguard_allow_hid Authorize Human Interface Devices and USB hubs in USBGuard daemon To allow authorization of USB devices combining human interface device and hub capabilities by USBGuard daemon, add the line allow with-interface match-all { 03:*:* 09:00:* } to /etc/usbguard/rules.conf. This rule should be understood primarily as a convenience administration feature. This rule ensures that if the USBGuard default rules.conf file is present, it will alter it so that USB human interface devices and hubs are allowed. However, if the rules.conf file is altered by system administrator, the rule does not check if USB human interface devices and hubs are allowed. This assumes that an administrator modified the file with some purpose in mind. CM-8(3) IA-3 FMT_SMF_EXT.1 SRG-OS-000114-GPOS-00059 SRG-APP-000092-CTR-000165 Without allowing Human Interface Devices, it might not be possible to interact with the system. Without allowing hubs, it might not be possible to use any USB devices on the system. # Remediation is applicable only in certain platforms if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel ); then echo "allow with-interface match-all { 03:*:* 09:00:* }" >> /etc/usbguard/rules.conf else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-8(3) - NIST-800-53-IA-3 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - usbguard_allow_hid_and_hub - name: Allow HID devices and hubs ansible.builtin.lineinfile: path: /etc/usbguard/rules.conf create: true regexp: '' line: allow with-interface match-all { 03:*:* 09:00:* } state: present when: ( ansible_architecture != "s390x" and "kernel" in ansible_facts.packages ) tags: - NIST-800-53-CM-8(3) - NIST-800-53-IA-3 - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - usbguard_allow_hid_and_hub --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig metadata: annotations: complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %0Aallow%20with-interface%20match-all%20%7B%2003%3A%2A%3A%2A%2009%3A00%3A%2A%20%7D }} mode: 0600 path: /etc/usbguard/rules.d/75-hid-and-hub.conf overwrite: true Authorize USB hubs in USBGuard daemon To allow authorization of USB hub devices by USBGuard daemon, add line allow with-interface match-all { 09:00:* } to /etc/usbguard/rules.conf. This rule should be understood primarily as a convenience administration feature. This rule ensures that if the USBGuard default rules.conf file is present, it will alter it so that USB hub devices are allowed. However, if the rules.conf file is altered by system administrator, the rule does not check if USB hub devices are allowed. This assumes that an administrator modified the file with some purpose in mind. SRG-OS-000114-GPOS-00059 Without allowing hubs, it might not be possible to use any USB devices on the system. # Remediation is applicable only in certain platforms if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel ); then echo "allow with-interface match-all { 09:00:* }" >> /etc/usbguard/rules.conf else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - usbguard_allow_hub - name: Allow hubs ansible.builtin.lineinfile: path: /etc/usbguard/rules.conf create: true regexp: '' line: allow with-interface match-all { 09:00:* } state: present when: ( ansible_architecture != "s390x" and "kernel" in ansible_facts.packages ) tags: - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - usbguard_allow_hub X Window System The X Window System implementation included with the system is called X.org. Disable X Windows Unless there is a mission-critical reason for the system to run a graphical user interface, ensure X is not set to start automatically at boot and remove the X Windows software packages. There is usually no reason to run X Windows on a dedicated server system, as it increases the system's attack surface and consumes system resources. Administrators of server systems should instead login via SSH or on the text console. Remove the X Windows Package Group By removing the xorg-x11-server-common package, the system no longer has X Windows installed. If X Windows is not installed then the system cannot boot into graphical user mode. This prevents the system from being accidentally or maliciously booted into a graphical.target mode. To do so, run the following command: $ sudo dnf groupremove "X Window System" $ sudo dnf remove xorg-x11-server-common The installation and use of a Graphical User Interface (GUI) increases your attack vector and decreases your overall security posture. Removing the package xorg-x11-server-common package will remove the graphical target which might bring your system to an inconsistent state requiring additional configuration to access the system again. If a GUI is an operational requirement, a tailored profile that removes this rule should used before continuing installation. 12 15 8 APO13.01 DSS01.04 DSS05.02 DSS05.03 4.3.3.6.6 SR 1.13 SR 2.6 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.13.1.1 A.13.2.1 A.14.1.3 A.6.2.1 A.6.2.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.PT-4 SRG-OS-000480-GPOS-00227 Unnecessary service packages must not be installed to decrease the attack surface of the system. X windows has a long history of security vulnerabilities and should not be installed unless approved and documented. # CAUTION: This remediation script will remove xorg-x11-server-common # from the system, and may remove any packages # that depend on xorg-x11-server-common. Execute this # remediation AFTER testing on a non-production # system! if rpm -q --quiet "xorg-x11-server-common" ; then dnf remove -y --noautoremove "xorg-x11-server-common" fi - name: 'Remove the X Windows Package Group: Ensure xorg-x11-server-common is removed' ansible.builtin.package: name: xorg-x11-server-common state: absent tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - disable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_xorg-x11-server-common_removed include remove_xorg-x11-server-common class remove_xorg-x11-server-common { package { 'xorg-x11-server-common': ensure => 'purged', } } package --remove=xorg-x11-server-common package remove xorg-x11-server-common dnf remove xorg-x11-server-common Disable Graphical Environment Startup By Setting Default Target Systems that do not require a graphical user interface should only boot by default into multi-user.target mode. This prevents accidental booting of the system into a graphical.target mode. Setting the system's default target to multi-user.target will prevent automatic startup of the graphical environment. To do so, run: $ systemctl set-default multi-user.target You should see the following output: Removed symlink /etc/systemd/system/default.target. Created symlink from /etc/systemd/system/default.target to /usr/lib/systemd/system/multi-user.target. 12 15 8 APO13.01 DSS01.04 DSS05.02 DSS05.03 4.3.3.6.6 SR 1.13 SR 2.6 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 7.1 SR 7.6 A.11.2.6 A.13.1.1 A.13.2.1 A.14.1.3 A.6.2.1 A.6.2.2 CM-7(a) CM-7(b) CM-6(a) PR.AC-3 PR.PT-4 SRG-OS-000480-GPOS-00227 2.1.22 Services that are not required for system and application processes must not be active to decrease the attack surface of the system. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then systemctl set-default multi-user.target else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - xwindows_runlevel_target - name: Switch to multi-user runlevel ansible.builtin.file: src: /usr/lib/systemd/system/multi-user.target dest: /etc/systemd/system/default.target state: link force: true when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - NIST-800-53-CM-7(a) - NIST-800-53-CM-7(b) - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - xwindows_runlevel_target System Accounting with auditd The audit service provides substantial capabilities for recording system activities. By default, the service audits about SELinux AVC denials and certain types of security-relevant events such as system logins, account modifications, and authentication events performed by programs such as sudo. Under its default configuration, auditd has modest disk space requirements, and should not noticeably impact system performance. NOTE: The Linux Audit daemon auditd can be configured to use the augenrules program to read audit rules files (*.rules) located in /etc/audit/rules.d location and compile them to create the resulting form of the /etc/audit/audit.rules configuration file during the daemon startup (default configuration). Alternatively, the auditd daemon can use the auditctl utility to read audit rules from the /etc/audit/audit.rules configuration file during daemon startup, and load them into the kernel. The expected behavior is configured via the appropriate ExecStartPost directive setting in the /usr/lib/systemd/system/auditd.service configuration file. To instruct the auditd daemon to use the augenrules program to read audit rules (default configuration), use the following setting: ExecStartPost=-/sbin/augenrules --load in the /usr/lib/systemd/system/auditd.service configuration file. In order to instruct the auditd daemon to use the auditctl utility to read audit rules, use the following setting: ExecStartPost=-/sbin/auditctl -R /etc/audit/audit.rules in the /usr/lib/systemd/system/auditd.service configuration file. Refer to [Service] section of the /usr/lib/systemd/system/auditd.service configuration file for further details. Government networks often have substantial auditing requirements and auditd can be configured to meet these requirements. Examining some example audit records demonstrates how the Linux audit system satisfies common requirements. The following example from Red Hat Enterprise Linux 7 Documentation available at https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html-single/selinux_users_and_administrators_guide/index#sect-Security-Enhanced_Linux-Fixing_Problems-Raw_Audit_Messages shows the substantial amount of information captured in a two typical "raw" audit messages, followed by a breakdown of the most important fields. In this example the message is SELinux-related and reports an AVC denial (and the associated system call) that occurred when the Apache HTTP Server attempted to access the /var/www/html/file1 file (labeled with the samba_share_t type): type=AVC msg=audit(1226874073.147:96): avc: denied { getattr } for pid=2465 comm="httpd" path="/var/www/html/file1" dev=dm-0 ino=284133 scontext=unconfined_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:samba_share_t:s0 tclass=file type=SYSCALL msg=audit(1226874073.147:96): arch=40000003 syscall=196 success=no exit=-13 a0=b98df198 a1=bfec85dc a2=54dff4 a3=2008171 items=0 ppid=2463 pid=2465 auid=502 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=6 comm="httpd" exe="/usr/sbin/httpd" subj=unconfined_u:system_r:httpd_t:s0 key=(null) msg=audit(1226874073.147:96)The number in parentheses is the unformatted time stamp (Epoch time) for the event, which can be converted to standard time by using the date command. { getattr }The item in braces indicates the permission that was denied. getattr indicates the source process was trying to read the target file's status information. This occurs before reading files. This action is denied due to the file being accessed having the wrong label. Commonly seen permissions include getattr, read, and write.comm="httpd"The executable that launched the process. The full path of the executable is found in the exe= section of the system call (SYSCALL) message, which in this case, is exe="/usr/sbin/httpd". path="/var/www/html/file1"The path to the object (target) the process attempted to access. scontext="unconfined_u:system_r:httpd_t:s0"The SELinux context of the process that attempted the denied action. In this case, it is the SELinux context of the Apache HTTP Server, which is running in the httpd_t domain. tcontext="unconfined_u:object_r:samba_share_t:s0"The SELinux context of the object (target) the process attempted to access. In this case, it is the SELinux context of file1. Note: the samba_share_t type is not accessible to processes running in the httpd_t domain. From the system call (SYSCALL) message, two items are of interest: success=no: indicates whether the denial (AVC) was enforced or not. success=no indicates the system call was not successful (SELinux denied access). success=yes indicates the system call was successful - this can be seen for permissive domains or unconfined domains, such as initrc_t and kernel_t. exe="/usr/sbin/httpd": the full path to the executable that launched the process, which in this case, is exe="/usr/sbin/httpd". Install audispd-plugins Package The audispd-plugins package can be installed with the following command: $ sudo dnf install audispd-plugins SRG-OS-000342-GPOS-00133 10.3.3 10.3 audispd-plugins provides plugins for the real-time interface to the audit subsystem, audispd. These plugins can do things like relay events to remote machines or analyze events for suspicious behavior. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "audispd-plugins" ; then dnf install -y "audispd-plugins" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audispd-plugins_installed - name: Ensure audispd-plugins is installed ansible.builtin.package: name: audispd-plugins state: present when: '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audispd-plugins_installed include install_audispd-plugins class install_audispd-plugins { package { 'audispd-plugins': ensure => 'installed', } } package --add=audispd-plugins [[packages]] name = "audispd-plugins" version = "*" package install audispd-plugins dnf install audispd-plugins Ensure the default plugins for the audit dispatcher are Installed The audit-audispd-plugins package should be installed. 164.308(a)(1)(ii)(D) 164.308(a)(5)(ii)(C) 164.310(a)(2)(iv) 164.310(d)(2)(iii) 164.312(b) Req-10.5.3 SRG-OS-000342-GPOS-00133 10.3.3 10.3 Information stored in one location is vulnerable to accidental or incidental deletion or alteration. Off-loading is a common process in information systems with limited audit storage capacity. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "audit-audispd-plugins" ; then dnf install -y "audit-audispd-plugins" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-10.5.3 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audit-audispd-plugins_installed - name: Ensure audit-audispd-plugins is installed ansible.builtin.package: name: audit-audispd-plugins state: present when: '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-10.5.3 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.3 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audit-audispd-plugins_installed include install_audit-audispd-plugins class install_audit-audispd-plugins { package { 'audit-audispd-plugins': ensure => 'installed', } } package --add=audit-audispd-plugins [[packages]] name = "audit-audispd-plugins" version = "*" package install audit-audispd-plugins dnf install audit-audispd-plugins Ensure the audit-libs package as a part of audit Subsystem is Installed The audit-libs package should be installed. CIP-004-6 R3.3 CIP-007-3 R6.5 AC-7(a) AU-7(1) AU-7(2) AU-14 AU-12(2) AU-2(a) CM-6(a) Req-10.2.1 SRG-OS-000062-GPOS-00031 SRG-OS-000037-GPOS-00015 SRG-OS-000038-GPOS-00016 SRG-OS-000039-GPOS-00017 SRG-OS-000040-GPOS-00018 SRG-OS-000041-GPOS-00019 SRG-OS-000042-GPOS-00021 SRG-OS-000051-GPOS-00024 SRG-OS-000054-GPOS-00025 SRG-OS-000122-GPOS-00063 SRG-OS-000254-GPOS-00095 SRG-OS-000255-GPOS-00096 SRG-OS-000337-GPOS-00129 SRG-OS-000348-GPOS-00136 SRG-OS-000349-GPOS-00137 SRG-OS-000350-GPOS-00138 SRG-OS-000351-GPOS-00139 SRG-OS-000352-GPOS-00140 SRG-OS-000353-GPOS-00141 SRG-OS-000354-GPOS-00142 SRG-OS-000358-GPOS-00145 SRG-OS-000365-GPOS-00152 SRG-OS-000392-GPOS-00172 SRG-OS-000475-GPOS-00220 6.3.1.1 The auditd service is an access monitoring and accounting daemon, watching system calls to audit any access, in comparison with potential local access control policy such as SELinux policy. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "audit-libs" ; then dnf install -y "audit-libs" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-7(a) - NIST-800-53-AU-12(2) - NIST-800-53-AU-14 - NIST-800-53-AU-2(a) - NIST-800-53-AU-7(1) - NIST-800-53-AU-7(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audit-libs_installed - name: Ensure audit-libs is installed ansible.builtin.package: name: audit-libs state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AU-12(2) - NIST-800-53-AU-14 - NIST-800-53-AU-2(a) - NIST-800-53-AU-7(1) - NIST-800-53-AU-7(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audit-libs_installed include install_audit-libs class install_audit-libs { package { 'audit-libs': ensure => 'installed', } } package --add=audit-libs [[packages]] name = "audit-libs" version = "*" package install audit-libs dnf install audit-libs Ensure the audit Subsystem is Installed The audit package should be installed. 164.308(a)(1)(ii)(D) 164.308(a)(5)(ii)(C) 164.310(a)(2)(iv) 164.310(d)(2)(iii) 164.312(b) CIP-004-6 R3.3 CIP-007-3 R6.5 AC-7(a) AU-7(1) AU-7(2) AU-14 AU-12(2) AU-2(a) CM-6(a) FAU_GEN.1 Req-10.1 SRG-OS-000062-GPOS-00031 SRG-OS-000037-GPOS-00015 SRG-OS-000038-GPOS-00016 SRG-OS-000039-GPOS-00017 SRG-OS-000040-GPOS-00018 SRG-OS-000041-GPOS-00019 SRG-OS-000042-GPOS-00021 SRG-OS-000051-GPOS-00024 SRG-OS-000054-GPOS-00025 SRG-OS-000122-GPOS-00063 SRG-OS-000254-GPOS-00095 SRG-OS-000255-GPOS-00096 SRG-OS-000337-GPOS-00129 SRG-OS-000348-GPOS-00136 SRG-OS-000349-GPOS-00137 SRG-OS-000350-GPOS-00138 SRG-OS-000351-GPOS-00139 SRG-OS-000352-GPOS-00140 SRG-OS-000353-GPOS-00141 SRG-OS-000354-GPOS-00142 SRG-OS-000358-GPOS-00145 SRG-OS-000365-GPOS-00152 SRG-OS-000392-GPOS-00172 SRG-OS-000475-GPOS-00220 R33 R73 6.3.1.1 10.2.1 10.2 The auditd service is an access monitoring and accounting daemon, watching system calls to audit any access, in comparison with potential local access control policy such as SELinux policy. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then if ! rpm -q --quiet "audit" ; then dnf install -y "audit" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-7(a) - NIST-800-53-AU-12(2) - NIST-800-53-AU-14 - NIST-800-53-AU-2(a) - NIST-800-53-AU-7(1) - NIST-800-53-AU-7(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.1 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audit_installed - name: Ensure audit is installed ansible.builtin.package: name: audit state: present when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-7(a) - NIST-800-53-AU-12(2) - NIST-800-53-AU-14 - NIST-800-53-AU-2(a) - NIST-800-53-AU-7(1) - NIST-800-53-AU-7(2) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.1 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - package_audit_installed include install_audit class install_audit { package { 'audit': ensure => 'installed', } } package --add=audit [[packages]] name = "audit" version = "*" package install audit dnf install audit Enable auditd Service The auditd service is an essential userspace component of the Linux Auditing System, as it is responsible for writing audit records to disk. The auditd service can be enabled with the following command: $ sudo systemctl enable auditd.service 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.3.1 3.3.2 3.3.6 164.308(a)(1)(ii)(D) 164.308(a)(5)(ii)(C) 164.310(a)(2)(iv) 164.310(d)(2)(iii) 164.312(b) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 CIP-004-6 R3.3 CIP-007-3 R6.5 AC-2(g) AU-3 AU-10 AU-2(d) AU-12(c) AU-14(1) AC-6(9) CM-6(a) SI-4(23) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 FAU_GEN.1 Req-10.1 SRG-OS-000062-GPOS-00031 SRG-OS-000037-GPOS-00015 SRG-OS-000038-GPOS-00016 SRG-OS-000039-GPOS-00017 SRG-OS-000040-GPOS-00018 SRG-OS-000041-GPOS-00019 SRG-OS-000042-GPOS-00021 SRG-OS-000051-GPOS-00024 SRG-OS-000054-GPOS-00025 SRG-OS-000122-GPOS-00063 SRG-OS-000254-GPOS-00095 SRG-OS-000255-GPOS-00096 SRG-OS-000337-GPOS-00129 SRG-OS-000348-GPOS-00136 SRG-OS-000349-GPOS-00137 SRG-OS-000350-GPOS-00138 SRG-OS-000351-GPOS-00139 SRG-OS-000352-GPOS-00140 SRG-OS-000353-GPOS-00141 SRG-OS-000354-GPOS-00142 SRG-OS-000358-GPOS-00145 SRG-OS-000365-GPOS-00152 SRG-OS-000392-GPOS-00172 SRG-OS-000475-GPOS-00220 SRG-APP-000095-CTR-000170 SRG-APP-000409-CTR-000990 SRG-APP-000508-CTR-001300 SRG-APP-000510-CTR-001310 R33 R73 6.3.1.4 10.2.1 10.2 Without establishing what type of events occurred, it would be difficult to establish, correlate, and investigate the events leading up to an outage or attack. Ensuring the auditd service is active ensures audit records generated by the kernel are appropriately recorded. Additionally, a properly configured audit subsystem ensures that actions of individual system users can be uniquely traced to those users so they can be held accountable for their actions. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q audit; }; then SYSTEMCTL_EXEC='/usr/bin/systemctl' "$SYSTEMCTL_EXEC" unmask 'auditd.service' if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then "$SYSTEMCTL_EXEC" start 'auditd.service' fi "$SYSTEMCTL_EXEC" enable 'auditd.service' else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-171-3.3.2 - NIST-800-171-3.3.6 - NIST-800-53-AC-2(g) - NIST-800-53-AC-6(9) - NIST-800-53-AU-10 - NIST-800-53-AU-12(c) - NIST-800-53-AU-14(1) - NIST-800-53-AU-2(d) - NIST-800-53-AU-3 - NIST-800-53-CM-6(a) - NIST-800-53-SI-4(23) - PCI-DSS-Req-10.1 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_auditd_enabled - name: Enable auditd Service - Enable service auditd block: - name: Gather the package facts ansible.builtin.package_facts: manager: auto - name: Enable auditd Service - Enable Service auditd ansible.builtin.systemd: name: auditd enabled: true state: started masked: false when: - '"audit" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-171-3.3.2 - NIST-800-171-3.3.6 - NIST-800-53-AC-2(g) - NIST-800-53-AC-6(9) - NIST-800-53-AU-10 - NIST-800-53-AU-12(c) - NIST-800-53-AU-14(1) - NIST-800-53-AU-2(d) - NIST-800-53-AU-3 - NIST-800-53-CM-6(a) - NIST-800-53-SI-4(23) - PCI-DSS-Req-10.1 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - enable_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - service_auditd_enabled - special_service_block when: - '"kernel" in ansible_facts.packages' - '"audit" in ansible_facts.packages' include enable_auditd class enable_auditd { service {'auditd': enable => true, ensure => 'running', } } --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 systemd: units: - name: auditd.service enabled: true [customizations.services] enabled = ["auditd"] service enable auditd Enable Auditing for Processes Which Start Prior to the Audit Daemon To ensure all processes can be audited, even those which start prior to the audit daemon, add the argument audit=1 to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain audit=1 as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) audit=1" 1 11 12 13 14 15 16 19 3 4 5 6 7 8 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.02 DSS05.03 DSS05.04 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.3.1 164.308(a)(1)(ii)(D) 164.308(a)(5)(ii)(C) 164.310(a)(2)(iv) 164.310(d)(2)(iii) 164.312(b) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AC-17(1) AU-14(1) AU-10 CM-6(a) IR-5(1) DE.AE-3 DE.AE-5 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 FAU_GEN.1 Req-10.3 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000473-GPOS-00218 SRG-OS-000254-GPOS-00095 6.3.1.2 10.7.2 10.7 Each process on the system carries an "auditable" flag which indicates whether its activities can be audited. Although auditd takes care of enabling this for all processes which launch after it does, adding the kernel argument ensures it is set for every process during boot. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q grub2-common; }; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "audit" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"audit=[^\"]*\"(.*]\s*)/\1\"audit=1\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"audit=1\"]" >> "$KARGS_DIR/10-audit.toml" fi else grubby --update-kernel=ALL --args=audit=1 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-17(1) - NIST-800-53-AU-10 - NIST-800-53-AU-14(1) - NIST-800-53-CM-6(a) - NIST-800-53-IR-5(1) - PCI-DSS-Req-10.3 - PCI-DSSv4-10.7 - PCI-DSSv4-10.7.2 - grub2_audit_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="audit=1" when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-17(1) - NIST-800-53-AU-10 - NIST-800-53-AU-14(1) - NIST-800-53-CM-6(a) - NIST-800-53-IR-5(1) - PCI-DSS-Req-10.3 - PCI-DSSv4-10.7 - PCI-DSSv4-10.7.2 - grub2_audit_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy [customizations.kernel] append = "audit=1" bootloader audit=1 Extend Audit Backlog Limit for the Audit Daemon To improve the kernel capacity to queue all log events, even those which occurred prior to the audit daemon, add the argument audit_backlog_limit=8192 to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain audit_backlog_limit=8192 as follows: # grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) audit_backlog_limit=8192" CM-6(a) FAU_STG.1 FAU_STG.3 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000254-GPOS-00095 SRG-OS-000341-GPOS-00132 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 6.3.1.3 10.7.2 10.7 audit_backlog_limit sets the queue length for audit events awaiting transfer to the audit daemon. Until the audit daemon is up and running, all log messages are stored in this queue. If the queue is overrun during boot process, the action defined by audit failure flag is taken. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel && { rpm --quiet -q grub2-common; }; then if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then KARGS_DIR="/usr/lib/bootc/kargs.d/" if grep -q -E "audit_backlog_limit" "$KARGS_DIR/*.toml" ; then sed -i -E "s/^(\s*kargs\s*=\s*\[.*)\"audit_backlog_limit=[^\"]*\"(.*]\s*)/\1\"audit_backlog_limit=8192\"\2/" "$KARGS_DIR/*.toml" else echo "kargs = [\"audit_backlog_limit=8192\"]" >> "$KARGS_DIR/10-audit_backlog_limit.toml" fi else grubby --update-kernel=ALL --args=audit_backlog_limit=8192 fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-10.7 - PCI-DSSv4-10.7.2 - grub2_audit_backlog_limit_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy - name: Update grub defaults and the bootloader menu ansible.builtin.command: /sbin/grubby --update-kernel=ALL --args="audit_backlog_limit=8192" when: - '"kernel" in ansible_facts.packages' - '"grub2-common" in ansible_facts.packages' tags: - NIST-800-53-CM-6(a) - PCI-DSSv4-10.7 - PCI-DSSv4-10.7.2 - grub2_audit_backlog_limit_argument - low_disruption - low_severity - medium_complexity - reboot_required - restrict_strategy [customizations.kernel] append = "audit_backlog_limit=8192" bootloader audit_backlog_limit=8192 Configure auditd Rules for Comprehensive Auditing The auditd program can perform comprehensive monitoring of system activity. This section describes recommended configuration settings for comprehensive auditing, but a full description of the auditing system's capabilities is beyond the scope of this guide. The mailing list linux-audit@redhat.com exists to facilitate community discussion of the auditing system. The audit subsystem supports extensive collection of events, including: Tracing of arbitrary system calls (identified by name or number) on entry or exit.Filtering by PID, UID, call success, system call argument (with some limitations), etc.Monitoring of specific files for modifications to the file's contents or metadata. Auditing rules at startup are controlled by the file /etc/audit/audit.rules. Add rules to it to meet the auditing requirements for your organization. Each line in /etc/audit/audit.rules represents a series of arguments that can be passed to auditctl and can be individually tested during runtime. See documentation in /usr/share/doc/audit-VERSION and in the related man pages for more details. If copying any example audit rulesets from /usr/share/doc/audit-VERSION, be sure to comment out the lines containing arch= which are not appropriate for your system's architecture. Then review and understand the following rules, ensuring rules are activated as needed for the appropriate architecture. After reviewing all the rules, reading the following sections, and editing as needed, the new rules can be activated as follows: $ sudo service auditd restart Record Events that Modify User/Group Information via open syscall - /etc/group The audit system should collect write events to /etc/group file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of groups through direct edition of /etc/group could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a1&03 -F path=/etc/group" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via open_by_handle_at syscall - /etc/group The audit system should collect write events to /etc/group file for all group and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of groups through direct edition of /etc/group could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/group" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open_by_handle_at" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open_by_handle_at tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via openat syscall - /etc/group The audit system should collect write events to /etc/group file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S openat -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of groups through direct edition of /etc/group could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/group" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="openat" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit openat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/group -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_group_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via open syscall - /etc/gshadow The audit system should collect write events to /etc/gshadow file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/gshadow could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a1&03 -F path=/etc/gshadow" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via open_by_handle_at syscall - /etc/gshadow The audit system should collect write events to /etc/gshadow file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/gshadow could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/gshadow" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open_by_handle_at" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open_by_handle_at tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via openat syscall - /etc/gshadow The audit system should collect write events to /etc/gshadow file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S openat -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=user-modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/gshadow could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/gshadow" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="openat" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit openat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/gshadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_gshadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via open syscall - /etc/passwd The audit system should collect write events to /etc/passwd file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/passwd could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a1&03 -F path=/etc/passwd" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via open_by_handle_at syscall - /etc/passwd The audit system should collect write events to /etc/passwd file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/passwd could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/passwd" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open_by_handle_at" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open_by_handle_at tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via openat syscall - /etc/passwd The audit system should collect write events to /etc/passwd file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S openat -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/passwd could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/passwd" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="openat" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit openat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/passwd -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_passwd_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via open syscall - /etc/shadow The audit system should collect write events to /etc/shadow file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open,openat,open_by_handle_at -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/shadow could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a1&03 -F path=/etc/shadow" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: [] - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a1&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via open_by_handle_at syscall - /etc/shadow The audit system should collect write events to /etc/shadow file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open_by_handle_at -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open,openat,open_by_handle_at -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/shadow could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/shadow" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open_by_handle_at" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open_by_handle_at tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: [] - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify User/Group Information via openat syscall - /etc/shadow The audit system should collect write events to /etc/shadow file for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S openat -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S openat -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open,openat,open_by_handle_at -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=user-modify CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Creation of users through direct edition of /etc/shadow could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a2&03 -F path=/etc/shadow" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="openat" KEY="user-modify" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit openat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modify.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modify.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: [] - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a2&03 -F path=/etc/shadow -F auid>=1000 -F auid!=unset -F key=modify create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_etc_shadow_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Make the auditd Configuration Immutable If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d in order to make the auditd configuration immutable: -e 2 If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file in order to make the auditd configuration immutable: -e 2 With this setting, a reboot will be required to change any audit rules. 1 11 12 13 14 15 16 18 19 3 4 5 6 7 8 5.4.1.1 APO01.06 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 DSS06.02 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.3.1 3.4.3 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.310(a)(2)(iv) 164.312(d) 164.310(d)(2)(iii) 164.312(b) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.7.3 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 5.2 SR 6.1 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 ID.SC-4 PR.AC-4 PR.DS-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.5.2 SRG-OS-000057-GPOS-00027 SRG-OS-000058-GPOS-00028 SRG-OS-000059-GPOS-00029 SRG-APP-000119-CTR-000245 SRG-APP-000120-CTR-000250 R73 6.3.3.33 10.3.2 10.3 Making the audit configuration immutable prevents accidental as well as malicious modification of the audit rules, although it may be problematic if legitimate changes are needed during system operation. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Traverse all of: # # /etc/audit/audit.rules, (for auditctl case) # /etc/audit/rules.d/*.rules (for augenrules case) # # files to check if '-e .*' setting is present in that '*.rules' file already. # If found, delete such occurrence since auditctl(8) manual page instructs the # '-e 2' rule should be placed as the last rule in the configuration find /etc/audit /etc/audit/rules.d -maxdepth 1 -type f -name '*.rules' -exec sed -i '/-e[[:space:]]\+.*/d' {} ';' # Append '-e 2' requirement at the end of both: # * /etc/audit/audit.rules file (for auditctl case) # * /etc/audit/rules.d/immutable.rules (for augenrules case) for AUDIT_FILE in "/etc/audit/audit.rules" "/etc/audit/rules.d/immutable.rules" do echo '' >> $AUDIT_FILE echo '# Set the audit.rules configuration immutable per security requirements' >> $AUDIT_FILE echo '# Reboot is required to change audit rules once this setting is applied' >> $AUDIT_FILE echo '-e 2' >> $AUDIT_FILE chmod o-rwx $AUDIT_FILE chmod g-rwx $AUDIT_FILE done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-171-3.4.3 - NIST-800-53-AC-6(9) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - audit_rules_immutable - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Collect all files from /etc/audit/rules.d with .rules extension ansible.builtin.find: paths: /etc/audit/rules.d/ patterns: '*.rules' register: find_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-171-3.4.3 - NIST-800-53-AC-6(9) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - audit_rules_immutable - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Remove the -e option from all Audit config files ansible.builtin.lineinfile: path: '{{ item }}' regexp: ^\s*(?:-e)\s+.*$ state: absent loop: '{{ find_rules_d.files | map(attribute=''path'') | list + [''/etc/audit/audit.rules''] }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-171-3.4.3 - NIST-800-53-AC-6(9) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - audit_rules_immutable - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Add Audit -e option into /etc/audit/rules.d/immutable.rules and /etc/audit/audit.rules ansible.builtin.lineinfile: path: '{{ item }}' create: true line: -e 2 mode: g-rwx,o-rwx loop: - /etc/audit/audit.rules - /etc/audit/rules.d/immutable.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-171-3.4.3 - NIST-800-53-AC-6(9) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.2 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.2 - audit_rules_immutable - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,-e%202%0A mode: 0600 path: /etc/audit/rules.d/90-immutable.rules overwrite: true Record Events that Modify the System's Mandatory Access Controls If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/selinux/ -p wa -k MAC-policy If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -w /etc/selinux/ -p wa -k MAC-policy 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.8 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 R73 10.3.4 10.3 The system's mandatory access policy (SELinux or Apparmor) should not be arbitrarily changed by anything other than administrator action. All changes to MAC policy should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/selinux/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/selinux/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/selinux/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/selinux/ -p wa -k MAC-policy" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/MAC-policy.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/selinux/" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/MAC-policy.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/MAC-policy.rules" # If the MAC-policy.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/selinux/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/selinux/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/selinux/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/selinux/ -p wa -k MAC-policy" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls - Check if watch rule for /etc/selinux/ already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/selinux/\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls - Search /etc/audit/rules.d for other rules with specified key MAC-policy ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)MAC-policy$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls - Use /etc/audit/rules.d/MAC-policy.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/MAC-policy.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls - Add watch rule for /etc/selinux/ in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/selinux/ -p wa -k MAC-policy create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls - Check if watch rule for /etc/selinux/ already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/selinux/\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls - Add watch rule for /etc/selinux/ in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/selinux/ -p wa -k MAC-policy state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ -w%20/etc/selinux/%20-p%20wa%20-k%20MAC-policy%0A }} mode: 0600 path: /etc/audit/rules.d/75-etcselinux-wa-MAC-policy.rules overwrite: true Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/selinux/ -p wa -k MAC-policy If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/selinux/ -p wa -k MAC-policy 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) Req-10.5.5 6.3.3.23 10.3.4 10.3 The system's mandatory access policy (SELinux) should not be arbitrarily changed by anything other than administrator action. All changes to MAC policy should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/selinux/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/selinux/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/selinux/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/selinux/ -p wa -k MAC-policy" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/MAC-policy.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/selinux/" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/MAC-policy.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/MAC-policy.rules" # If the MAC-policy.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/selinux/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/selinux/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/selinux/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/selinux/ -p wa -k MAC-policy" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) - Check if watch rule for /etc/selinux/ already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/selinux/\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) - Search /etc/audit/rules.d for other rules with specified key MAC-policy ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)MAC-policy$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) - Use /etc/audit/rules.d/MAC-policy.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/MAC-policy.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) - Add watch rule for /etc/selinux/ in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/selinux/ -p wa -k MAC-policy create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) - Check if watch rule for /etc/selinux/ already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/selinux/\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls (/etc/selinux) - Add watch rule for /etc/selinux/ in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/selinux/ -p wa -k MAC-policy state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_mac_modification_etc_selinux - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events that Modify the System's Mandatory Access Controls in usr/share If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /usr/share/selinux/ -p wa -k MAC-policy If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /usr/share/selinux/ -p wa -k MAC-policy APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.8 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 6.3.3.23 The system's mandatory access policy (SELinux) should not be arbitrarily changed by anything other than administrator action. All changes to MAC policy should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/usr/share/selinux/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/usr/share/selinux/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/usr/share/selinux/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /usr/share/selinux/ -p wa -k MAC-policy" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/MAC-policy.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/usr/share/selinux/" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/MAC-policy.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/MAC-policy.rules" # If the MAC-policy.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/usr/share/selinux/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/usr/share/selinux/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/usr/share/selinux/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /usr/share/selinux/ -p wa -k MAC-policy" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls in usr/share - Check if watch rule for /usr/share/selinux/ already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/usr/share/selinux/\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls in usr/share - Search /etc/audit/rules.d for other rules with specified key MAC-policy ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)MAC-policy$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls in usr/share - Use /etc/audit/rules.d/MAC-policy.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/MAC-policy.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls in usr/share - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls in usr/share - Add watch rule for /usr/share/selinux/ in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /usr/share/selinux/ -p wa -k MAC-policy create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls in usr/share - Check if watch rule for /usr/share/selinux/ already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/usr/share/selinux/\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Mandatory Access Controls in usr/share - Add watch rule for /usr/share/selinux/ in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /usr/share/selinux/ -p wa -k MAC-policy state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - NIST-800-171-3.1.8 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - audit_rules_mac_modification_usr_share - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on Exporting to Media (successful) At a minimum, the audit system should collect media exportation events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 R73 6.3.3.19 10.2.1.7 10.2.1 10.2 The unauthorized exportation of data to external media could result in an information leak where classified information, Privacy Act information, and intellectual property could be lost. An audit trail should be created each time a filesystem is mounted to help identify and guard against information loss. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="mount" KEY="export" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_media_export - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit mount tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_media_export - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for mount for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - mount syscall_grouping: [] - name: Check existence of mount in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/export.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/export.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=export create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - mount syscall_grouping: [] - name: Check existence of mount in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=export create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_media_export - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for mount for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - mount syscall_grouping: [] - name: Check existence of mount in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/export.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/export.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=export create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - mount syscall_grouping: [] - name: Check existence of mount in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=export create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_media_export - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Network Environment If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S sethostname,setdomainname -F key=audit_rules_networkconfig_modification -w /etc/issue -p wa -k audit_rules_networkconfig_modification -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification -w /etc/hosts -p wa -k audit_rules_networkconfig_modification -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S sethostname,setdomainname -F key=audit_rules_networkconfig_modification -w /etc/issue -p wa -k audit_rules_networkconfig_modification -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification -w /etc/hosts -p wa -k audit_rules_networkconfig_modification -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 R73 6.3.3.7 10.3.4 10.3 The network environment should not be modified by anything other than administrator action. Any change to network parameters should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="" SYSCALL="sethostname setdomainname" KEY="audit_rules_networkconfig_modification" SYSCALL_GROUPING="sethostname setdomainname" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done # Then perform the remediations for the watch rules # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/issue" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/issue $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/issue$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/issue -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/issue" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules" # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/issue" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/issue $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/issue$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/issue -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/issue.net" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/issue.net $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/issue.net$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/issue.net -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/issue.net" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules" # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/issue.net" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/issue.net $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/issue.net$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/issue.net -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/hosts" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/hosts $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/hosts$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/hosts -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/hosts" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules" # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/hosts" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/hosts $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/hosts$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/hosts -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sysconfig/network" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sysconfig/network $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sysconfig/network$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sysconfig/network" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_networkconfig_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules" # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sysconfig/network" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sysconfig/network $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sysconfig/network$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set architecture for audit tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Remediate audit rules for network configuration for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - sethostname - setdomainname syscall_grouping: - sethostname - setdomainname - name: Check existence of sethostname, setdomainname in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/audit_rules_networkconfig_modification.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - sethostname - setdomainname syscall_grouping: - sethostname - setdomainname - name: Check existence of sethostname, setdomainname in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Remediate audit rules for network configuration for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - sethostname - setdomainname syscall_grouping: - sethostname - setdomainname - name: Check existence of sethostname, setdomainname in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/audit_rules_networkconfig_modification.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - sethostname - setdomainname syscall_grouping: - sethostname - setdomainname - name: Check existence of sethostname, setdomainname in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/issue already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/issue\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/issue in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/issue -p wa -k audit_rules_networkconfig_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/issue already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/issue\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/issue in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/issue -p wa -k audit_rules_networkconfig_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/issue.net already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/issue.net\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/issue.net in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/issue.net already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/issue.net\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/issue.net in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/hosts already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/hosts\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/hosts in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/hosts -p wa -k audit_rules_networkconfig_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/hosts already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/hosts\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/hosts in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/hosts -p wa -k audit_rules_networkconfig_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/sysconfig/network already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/sysconfig/network\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/sysconfig/network in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/sysconfig/network already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/sysconfig/network\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/sysconfig/network in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_networkconfig_modification - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events that Modify the System's Network Environment If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/sysconfig/network-scripts -p wa -k audit_rules_networkconfig_modification_network_scripts If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/sysconfig/network-scripts -p wa -k audit_rules_networkconfig_modification_network_scripts 6.3.3.7 The network environment should not be modified by anything other than administrator action. Any change to network parameters should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sysconfig/network-scripts" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sysconfig/network-scripts $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sysconfig/network-scripts$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sysconfig/network-scripts -p wa -k audit_rules_networkconfig_modification_network_scripts" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_networkconfig_modification_network_scripts.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sysconfig/network-scripts" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_networkconfig_modification_network_scripts.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification_network_scripts.rules" # If the audit_rules_networkconfig_modification_network_scripts.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sysconfig/network-scripts" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sysconfig/network-scripts $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sysconfig/network-scripts$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sysconfig/network-scripts -p wa -k audit_rules_networkconfig_modification_network_scripts" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/sysconfig/network-scripts already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/sysconfig/network-scripts\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification_network_scripts ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification_network_scripts$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use /etc/audit/rules.d/audit_rules_networkconfig_modification_network_scripts.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_networkconfig_modification_network_scripts.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/sysconfig/network-scripts in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/sysconfig/network-scripts -p wa -k audit_rules_networkconfig_modification_network_scripts create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Check if watch rule for /etc/sysconfig/network-scripts already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/sysconfig/network-scripts\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify the System's Network Environment - Add watch rule for /etc/sysconfig/network-scripts in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/sysconfig/network-scripts -p wa -k audit_rules_networkconfig_modification_network_scripts state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - audit_rules_networkconfig_modification_network_scripts - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Attempts to Alter Process and Session Initiation Information The audit system already collects process information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d in order to watch for attempted manual edits of files involved in storing such process information: -w /var/run/utmp -p wa -k session -w /var/log/btmp -p wa -k session -w /var/log/wtmp -p wa -k session If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file in order to watch for attempted manual edits of files involved in storing such process information: -w /var/run/utmp -p wa -k session -w /var/log/btmp -p wa -k session -w /var/log/wtmp -p wa -k session 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 0582 0584 05885 0586 0846 0957 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.3 SRG-APP-000505-CTR-001285 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/run/utmp" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/session.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/session.rules" # If the session.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/btmp" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/session.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/session.rules" # If the session.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/wtmp" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/session.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/session.rules" # If the session.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Check if watch rule for /var/run/utmp already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Search /etc/audit/rules.d for other rules with specified key session ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)session$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Use /etc/audit/rules.d/session.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/session.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Add watch rule for /var/run/utmp in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/run/utmp -p wa -k session create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Check if watch rule for /var/run/utmp already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Add watch rule for /var/run/utmp in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/run/utmp -p wa -k session state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Check if watch rule for /var/log/btmp already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Search /etc/audit/rules.d for other rules with specified key session ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)session$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Use /etc/audit/rules.d/session.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/session.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Add watch rule for /var/log/btmp in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/log/btmp -p wa -k session create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Check if watch rule for /var/log/btmp already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Add watch rule for /var/log/btmp in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/log/btmp -p wa -k session state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Check if watch rule for /var/log/wtmp already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Search /etc/audit/rules.d for other rules with specified key session ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)session$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Use /etc/audit/rules.d/session.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/session.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Add watch rule for /var/log/wtmp in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/log/wtmp -p wa -k session create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Check if watch rule for /var/log/wtmp already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information - Add watch rule for /var/log/wtmp in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/log/wtmp -p wa -k session state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - audit_rules_session_events - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %0A-w%20/var/run/utmp%20-p%20wa%20-k%20session%0A-w%20/var/log/btmp%20-p%20wa%20-k%20session%0A-w%20/var/log/wtmp%20-p%20wa%20-k%20session%0A }} mode: 0600 path: /etc/audit/rules.d/75-audit-session-events.rules overwrite: true Record Attempts to Alter Process and Session Initiation Information btmp The audit system already collects process information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /var/log/btmp -p wa -k session If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /var/log/btmp -p wa -k session 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) AU-12(c) AU-12.1(iv) SRG-OS-000472-GPOS-00217 R73 6.3.3.20 10.2.1.3 10.2.1 10.2 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/btmp" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/session.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/session.rules" # If the session.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information btmp - Check if watch rule for /var/log/btmp already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information btmp - Search /etc/audit/rules.d for other rules with specified key session ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)session$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information btmp - Use /etc/audit/rules.d/session.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/session.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information btmp - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information btmp - Add watch rule for /var/log/btmp in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/log/btmp -p wa -k session create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information btmp - Check if watch rule for /var/log/btmp already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information btmp - Add watch rule for /var/log/btmp in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/log/btmp -p wa -k session state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_btmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Attempts to Alter Process and Session Initiation Information utmp The audit system already collects process information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /var/run/utmp -p wa -k session If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /var/run/utmp -p wa -k session 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) AU-12(c) AU-12.1(iv) SRG-OS-000472-GPOS-00217 R73 6.3.3.20 10.2.1.3 10.2.1 10.2 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/run/utmp" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/session.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/session.rules" # If the session.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information utmp - Check if watch rule for /var/run/utmp already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information utmp - Search /etc/audit/rules.d for other rules with specified key session ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)session$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information utmp - Use /etc/audit/rules.d/session.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/session.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information utmp - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information utmp - Add watch rule for /var/run/utmp in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/run/utmp -p wa -k session create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information utmp - Check if watch rule for /var/run/utmp already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information utmp - Add watch rule for /var/run/utmp in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/run/utmp -p wa -k session state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_utmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Attempts to Alter Process and Session Initiation Information wtmp The audit system already collects process information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /var/log/wtmp -p wa -k session If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /var/log/wtmp -p wa -k session 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) AU-12(c) AU-12.1(iv) SRG-OS-000472-GPOS-00217 R73 6.3.3.20 10.2.1.3 10.2.1 10.2 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/wtmp" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/session.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/session.rules" # If the session.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information wtmp - Check if watch rule for /var/log/wtmp already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information wtmp - Search /etc/audit/rules.d for other rules with specified key session ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)session$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information wtmp - Use /etc/audit/rules.d/session.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/session.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information wtmp - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information wtmp - Add watch rule for /var/log/wtmp in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/log/wtmp -p wa -k session create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information wtmp - Check if watch rule for /var/log/wtmp already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Process and Session Initiation Information wtmp - Add watch rule for /var/log/wtmp in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/log/wtmp -p wa -k session state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(iv) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_session_events_wtmp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects System Administrator Actions - /etc/sudoers At a minimum, the audit system should collect administrator actions for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/sudoers -p wa -k actions If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/sudoers -p wa -k actions SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000503-CTR-001275 The actions taken by system administrators should be audited to keep a record of what was executed on the system, as well as, for accountability purposes. Editing the sudoers file may be sign of an attacker trying to establish persistent methods to a system, auditing the editing of the sudoers files mitigates this risk. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/actions.rules" # If the actions.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers - Check if watch rule for /etc/sudoers already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers - Search /etc/audit/rules.d for other rules with specified key actions ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)actions$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers - Use /etc/audit/rules.d/actions.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/actions.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers - Add watch rule for /etc/sudoers in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/sudoers -p wa -k actions create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers - Check if watch rule for /etc/sudoers already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers - Add watch rule for /etc/sudoers in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/sudoers -p wa -k actions state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - audit_rules_sudoers - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ At a minimum, the audit system should collect administrator actions for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/sudoers.d/ -p wa -k actions If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/sudoers.d/ -p wa -k actions SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000503-CTR-001275 The actions taken by system administrators should be audited to keep a record of what was executed on the system, as well as, for accountability purposes. Editing the sudoers file may be sign of an attacker trying to establish persistent methods to a system, auditing the editing of the sudoers files mitigates this risk. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers.d/" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/actions.rules" # If the actions.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ - Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ - Search /etc/audit/rules.d for other rules with specified key actions ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)actions$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ - Use /etc/audit/rules.d/actions.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/actions.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ - Add watch rule for /etc/sudoers.d/ in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/sudoers.d/ -p wa -k actions create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ - Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/ - Add watch rule for /etc/sudoers.d/ in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/sudoers.d/ -p wa -k actions state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - audit_rules_sudoers_d - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events When Executables Are Run As Another User Verify the system generates an audit record when actions are run as another user. sudo provides users with temporary elevated privileges to perform operations, either as the superuser or another user. If audit is using the "auditctl" tool to load the rules, run the following command: $ sudo grep execve /etc/audit/audit.rules If audit is using the "augenrules" tool to load the rules, run the following command: $ sudo grep -r execve /etc/audit/rules.d -a always,exit -F arch=b32 -S execve -C euid!=uid -F auid!=unset -k user_emulation -a always,exit -F arch=b64 S execve -C euid!=uid -F auid!=unset -k user_emulation If both the "b32" and "b64" audit rules for "SUID" files are not defined, this is a finding. Note that these rules can be configured in a number of ways while still achieving the desired effect. 6.3.3.2 Creating an audit log of users with temporary elevated privileges and the operation(s) they performed is essential to reporting. Administrators will want to correlate the events written to the audit trail with the records written to sudo's logfile to verify if unauthorized commands have been executed. Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised information system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider threats and the advanced persistent threat. Record Events When Privileged Executables Are Run Verify the system generates an audit record when privileged functions are executed. If audit is using the "auditctl" tool to load the rules, run the following command: $ sudo grep execve /etc/audit/audit.rules If audit is using the "augenrules" tool to load the rules, run the following command: $ sudo grep -r execve /etc/audit/rules.d -a always,exit -F arch=b32 -S execve -C uid!=euid -F euid=0 -k setuid -a always,exit -F arch=b64 -S execve -C uid!=euid -F euid=0 -k setuid -a always,exit -F arch=b32 -S execve -C gid!=egid -F egid=0 -k setgid -a always,exit -F arch=b64 -S execve -C gid!=egid -F egid=0 -k setgid If both the "b32" and "b64" audit rules for "SUID" files are not defined, this is a finding. If both the "b32" and "b64" audit rules for "SGID" files are not defined, this is a finding. Note that these rules can be configured in a number of ways while still achieving the desired effect. CM-5(1) AU-7(a) AU-7(b) AU-8(b) AU-12(3) AC-6(9) SRG-OS-000326-GPOS-00126 SRG-OS-000327-GPOS-00127 SRG-APP-000343-CTR-000780 SRG-APP-000381-CTR-000905 SRG-OS-000755-GPOS-00220 10.2.1.2 10.2.1 10.2 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised information system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider threats and the advanced persistent threat. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-C uid!=euid -F euid=0" AUID_FILTERS="" SYSCALL="execve" KEY="setuid" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-C gid!=egid -F egid=0" AUID_FILTERS="" SYSCALL="execve" KEY="setgid" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(3) - NIST-800-53-AU-7(a) - NIST-800-53-AU-7(b) - NIST-800-53-AU-8(b) - NIST-800-53-CM-5(1) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.2 - audit_rules_suid_privilege_function - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Service facts ansible.builtin.service_facts: null when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(3) - NIST-800-53-AU-7(a) - NIST-800-53-AU-7(b) - NIST-800-53-AU-8(b) - NIST-800-53-CM-5(1) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.2 - audit_rules_suid_privilege_function - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set suid_audit_rules fact ansible.builtin.set_fact: suid_audit_rules: - rule: -a always,exit -F arch=b32 -S execve -C gid!=egid -F egid=0 -k setgid regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b32[\s]+-S[\s]+execve[\s]+-C[\s]+gid!=egid[\s]+-F[\s]+egid=0[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$ - rule: -a always,exit -F arch=b64 -S execve -C gid!=egid -F egid=0 -k setgid regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b64[\s]+-S[\s]+execve[\s]+-C[\s]+gid!=egid[\s]+-F[\s]+egid=0[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$ - rule: -a always,exit -F arch=b32 -S execve -C uid!=euid -F euid=0 -k setuid regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b32[\s]+-S[\s]+execve[\s]+-C[\s]+uid!=euid[\s]+-F[\s]+euid=0[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$ - rule: -a always,exit -F arch=b64 -S execve -C uid!=euid -F euid=0 -k setuid regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b64[\s]+-S[\s]+execve[\s]+-C[\s]+uid!=euid[\s]+-F[\s]+euid=0[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$ when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(3) - NIST-800-53-AU-7(a) - NIST-800-53-AU-7(b) - NIST-800-53-AU-8(b) - NIST-800-53-CM-5(1) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.2 - audit_rules_suid_privilege_function - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Update /etc/audit/rules.d/privileged.rules to audit privileged functions ansible.builtin.lineinfile: path: /etc/audit/rules.d/privileged.rules line: '{{ item.rule }}' regexp: '{{ item.regex }}' mode: '0600' create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ('"auditd.service" in ansible_facts.services' or '"augenrules.service" in ansible_facts.services') register: augenrules_audit_rules_privilege_function_update_result with_items: '{{ suid_audit_rules }}' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(3) - NIST-800-53-AU-7(a) - NIST-800-53-AU-7(b) - NIST-800-53-AU-8(b) - NIST-800-53-CM-5(1) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.2 - audit_rules_suid_privilege_function - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Update /etc/audit/audit.rules to audit privileged functions ansible.builtin.lineinfile: path: /etc/audit/audit.rules line: '{{ item.rule }}' regexp: '{{ item.regex }}' create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ('"auditd.service" in ansible_facts.services' or '"augenrules.service" in ansible_facts.services') register: auditctl_audit_rules_privilege_function_update_result with_items: '{{ suid_audit_rules }}' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(3) - NIST-800-53-AU-7(a) - NIST-800-53-AU-7(b) - NIST-800-53-AU-8(b) - NIST-800-53-CM-5(1) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.2 - audit_rules_suid_privilege_function - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Restart Auditd ansible.builtin.command: /usr/sbin/service auditd restart when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - (augenrules_audit_rules_privilege_function_update_result.changed or auditctl_audit_rules_privilege_function_update_result.changed) - ansible_facts.services["auditd.service"].state == "running" tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(3) - NIST-800-53-AU-7(a) - NIST-800-53-AU-7(b) - NIST-800-53-AU-8(b) - NIST-800-53-CM-5(1) - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.2 - audit_rules_suid_privilege_function - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db32%20-S%20execve%20-C%20uid%21%3Deuid%20-F%20euid%3D0%20-k%20execpriv%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20execve%20-C%20uid%21%3Deuid%20-F%20euid%3D0%20-k%20execpriv%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20execve%20-C%20gid%21%3Degid%20-F%20egid%3D0%20-k%20execpriv%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20execve%20-C%20gid%21%3Degid%20-F%20egid%3D0%20-k%20execpriv%0A }} mode: 0600 path: /etc/audit/rules.d/75-audit-suid-privilege-function.rules overwrite: true Ensure auditd Collects System Administrator Actions If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/sudoers -p wa -k actions If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/sudoers -p wa -k actions If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/sudoers.d/ -p wa -k actions If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/sudoers.d/ -p wa -k actions 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.2.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.8 4.3.3.6.6 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 AC-2(7)(b) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-1 PR.AC-3 PR.AC-4 PR.AC-6 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.2 Req-10.2.5.b SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000304-GPOS-00121 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-APP-000026-CTR-000070 SRG-APP-000027-CTR-000075 SRG-APP-000028-CTR-000080 SRG-APP-000291-CTR-000675 SRG-APP-000292-CTR-000680 SRG-APP-000293-CTR-000685 SRG-APP-000294-CTR-000690 SRG-APP-000319-CTR-000745 SRG-APP-000320-CTR-000750 SRG-APP-000509-CTR-001305 R73 6.3.3.1 10.2.1.5 10.2.1 10.2 The actions taken by system administrators should be audited to keep a record of what was executed on the system, as well as, for accountability purposes. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/actions.rules" # If the actions.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers.d/" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/actions.rules" # If the actions.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Check if watch rule for /etc/sudoers already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/sudoers -p wa -k actions state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Check if watch rule for /etc/sudoers already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Search /etc/audit/rules.d for other rules with specified key actions ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)actions$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Use /etc/audit/rules.d/actions.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/actions.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/sudoers -p wa -k actions create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers.d/ in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/sudoers.d/ -p wa -k actions state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Search /etc/audit/rules.d for other rules with specified key actions ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)actions$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Use /etc/audit/rules.d/actions.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/actions.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers.d/ in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/sudoers.d/ -p wa -k actions create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(7)(b) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_sysadmin_actions - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ -w%20/etc/sudoers.d/%20-p%20wa%20-k%20actions%0A-w%20/etc/sudoers%20-p%20wa%20-k%20actions%0A }} mode: 0600 path: /etc/audit/rules.d/75-audit-sysadmin-actions.rules overwrite: true Record Events that Modify User/Group Information If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes: -w /etc/group -p wa -k audit_rules_usergroup_modification -w /etc/passwd -p wa -k audit_rules_usergroup_modification -w /etc/gshadow -p wa -k audit_rules_usergroup_modification -w /etc/shadow -p wa -k audit_rules_usergroup_modification -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes: -w /etc/group -p wa -k audit_rules_usergroup_modification -w /etc/passwd -p wa -k audit_rules_usergroup_modification -w /etc/gshadow -p wa -k audit_rules_usergroup_modification -w /etc/shadow -p wa -k audit_rules_usergroup_modification -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification This rule checks for multiple syscalls related to account changes; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: audit_rules_usergroup_modification_groupaudit_rules_usergroup_modification_gshadowaudit_rules_usergroup_modification_passwd 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 4.2.3.10 4.3.2.6.7 4.3.3.2.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.8 4.3.3.6.6 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-1 PR.AC-3 PR.AC-4 PR.AC-6 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.5 SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000239-GPOS-00089 SRG-OS-000241-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000476-GPOS-00221 SRG-APP-000026-CTR-000070 SRG-APP-000027-CTR-000075 SRG-APP-000028-CTR-000080 SRG-APP-000291-CTR-000675 SRG-APP-000292-CTR-000680 SRG-APP-000293-CTR-000685 SRG-APP-000294-CTR-000690 SRG-APP-000319-CTR-000745 SRG-APP-000320-CTR-000750 SRG-APP-000509-CTR-001305 In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy. Record Events that Modify User/Group Information - /etc/group If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/group -p wa -k audit_rules_usergroup_modification If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/group -p wa -k audit_rules_usergroup_modification 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.2.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.8 4.3.3.6.6 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-1 PR.AC-3 PR.AC-4 PR.AC-6 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.5 SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000503-CTR-001275 R73 6.3.3.12 10.2.1.5 10.2.1 10.2 In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/group" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/group" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules" # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/group" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/group - Check if watch rule for /etc/group already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/group\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/group - Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/group - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_usergroup_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/group - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/group - Add watch rule for /etc/group in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/group -p wa -k audit_rules_usergroup_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/group - Check if watch rule for /etc/group already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/group\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/group - Add watch rule for /etc/group in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/group -p wa -k audit_rules_usergroup_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_group - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events that Modify User/Group Information - /etc/gshadow If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.2.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.8 4.3.3.6.6 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-1 PR.AC-3 PR.AC-4 PR.AC-6 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.5 SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000503-CTR-001275 R73 6.3.3.14 10.2.1.5 10.2.1 10.2 In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/gshadow" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/gshadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/gshadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/gshadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/gshadow" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules" # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/gshadow" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/gshadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/gshadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/gshadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/gshadow - Check if watch rule for /etc/gshadow already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/gshadow\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/gshadow - Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/gshadow - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_usergroup_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/gshadow - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/gshadow - Add watch rule for /etc/gshadow in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/gshadow - Check if watch rule for /etc/gshadow already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/gshadow\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/gshadow - Add watch rule for /etc/gshadow in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_gshadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events that Modify User/Group Information - /etc/security/opasswd If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.2.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.8 4.3.3.6.6 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-1 PR.AC-3 PR.AC-4 PR.AC-6 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.5 SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 SRG-APP-000503-CTR-001275 R73 6.3.3.15 10.2.1.5 10.2.1 10.2 In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/security/opasswd" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/security/opasswd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/security/opasswd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/security/opasswd" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules" # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/security/opasswd" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/security/opasswd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/security/opasswd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/security/opasswd - Check if watch rule for /etc/security/opasswd already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/security/opasswd\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/security/opasswd - Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/security/opasswd - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_usergroup_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/security/opasswd - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/security/opasswd - Add watch rule for /etc/security/opasswd in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/security/opasswd - Check if watch rule for /etc/security/opasswd already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/security/opasswd\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/security/opasswd - Add watch rule for /etc/security/opasswd in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_opasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events that Modify User/Group Information - /etc/passwd If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/passwd -p wa -k audit_rules_usergroup_modification If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/passwd -p wa -k audit_rules_usergroup_modification 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.2.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.8 4.3.3.6.6 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-1 PR.AC-3 PR.AC-4 PR.AC-6 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.5 SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000304-GPOS-00121 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-OS-000274-GPOS-00104 SRG-OS-000275-GPOS-00105 SRG-OS-000276-GPOS-00106 SRG-OS-000277-GPOS-00107 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000503-CTR-001275 R73 6.3.3.13 10.2.1.5 10.2.1 10.2 In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/passwd" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/passwd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/passwd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/passwd -p wa -k audit_rules_usergroup_modification_passwd" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification_passwd.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/passwd" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_usergroup_modification_passwd.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification_passwd.rules" # If the audit_rules_usergroup_modification_passwd.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/passwd" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/passwd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/passwd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/passwd -p wa -k audit_rules_usergroup_modification_passwd" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/passwd - Check if watch rule for /etc/passwd already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/passwd\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/passwd - Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification_passwd ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification_passwd$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/passwd - Use /etc/audit/rules.d/audit_rules_usergroup_modification_passwd.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_usergroup_modification_passwd.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/passwd - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/passwd - Add watch rule for /etc/passwd in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification_passwd create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/passwd - Check if watch rule for /etc/passwd already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/passwd\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/passwd - Add watch rule for /etc/passwd in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification_passwd state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events that Modify User/Group Information - /etc/shadow If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/shadow -p wa -k audit_rules_usergroup_modification If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/shadow -p wa -k audit_rules_usergroup_modification 1 11 12 13 14 15 16 18 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 DSS06.03 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.2.2 4.3.3.3.9 4.3.3.5.1 4.3.3.5.2 4.3.3.5.8 4.3.3.6.6 4.3.3.7.2 4.3.3.7.3 4.3.3.7.4 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.1 SR 1.13 SR 1.2 SR 1.3 SR 1.4 SR 1.5 SR 1.7 SR 1.8 SR 1.9 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.6.2.1 A.6.2.2 A.7.1.1 A.9.1.2 A.9.2.1 A.9.2.2 A.9.2.3 A.9.2.4 A.9.2.6 A.9.3.1 A.9.4.1 A.9.4.2 A.9.4.3 A.9.4.4 A.9.4.5 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-1 PR.AC-3 PR.AC-4 PR.AC-6 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.5 SRG-OS-000004-GPOS-00004 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000304-GPOS-00121 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000470-GPOS-00214 SRG-OS-000471-GPOS-00215 SRG-OS-000239-GPOS-00089 SRG-OS-000240-GPOS-00090 SRG-OS-000241-GPOS-00091 SRG-OS-000303-GPOS-00120 SRG-OS-000466-GPOS-00210 SRG-OS-000476-GPOS-00221 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000503-CTR-001275 R73 6.3.3.14 10.2.1.5 10.2.1 10.2 In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/shadow" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/shadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/shadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/shadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/shadow" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules" # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/shadow" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/shadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/shadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/shadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/shadow - Check if watch rule for /etc/shadow already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/shadow\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/shadow - Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/shadow - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_rules_usergroup_modification.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/shadow - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/shadow - Add watch rule for /etc/shadow in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/shadow - Check if watch rule for /etc/shadow already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/shadow\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Events that Modify User/Group Information - /etc/shadow - Add watch rule for /etc/shadow in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.5 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.5 - audit_rules_usergroup_modification_shadow - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Attempts to perform maintenance activities The Fedora operating system must generate audit records for privileged activities, nonlocal maintenance, diagnostic sessions and other system-level access. Verify the operating system audits activities performed during nonlocal maintenance and diagnostic sessions. Run the following command: $ sudo auditctl -l | grep sudo.log -w /var/log/sudo.log -p wa -k maintenance If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /var/log/sudo.log -p wa -k maintenance If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /var/log/sudo.log -p wa -k maintenance Req-10.2.2 Req-10.2.5.b SRG-OS-000392-GPOS-00172 SRG-OS-000471-GPOS-00215 R73 6.3.3.3 10.2.1.3 10.2.1 10.2 If events associated with nonlocal administrative access or diagnostic sessions are not logged, a major tool for assessing and investigating attacks would not be available. This requirement addresses auditing-related issues associated with maintenance tools used specifically for diagnostic and repair actions on organizational information systems. Nonlocal maintenance and diagnostic activities are those activities conducted by individuals communicating through a network, either an external network (e.g., the internet) or an internal network. Local maintenance and diagnostic activities are those activities carried out by individuals physically present at the information system or information system component and not communicating across a network connection. This requirement applies to hardware/software diagnostic test equipment or tools. This requirement does not cover hardware/software components that may support information system maintenance, yet are a part of the system, for example, the software implementing "ping," "ls," "ipconfig," or the hardware and software implementing the monitoring port of an Ethernet switch. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/sudo.log" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/sudo.log $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/sudo.log$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/sudo.log -p wa -k maintenance" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/maintenance.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/sudo.log" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/maintenance.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/maintenance.rules" # If the maintenance.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/sudo.log" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/sudo.log $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/sudo.log$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/sudo.log -p wa -k maintenance" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to perform maintenance activities - Check if watch rule for /var/log/sudo.log already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/log/sudo.log\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to perform maintenance activities - Search /etc/audit/rules.d for other rules with specified key maintenance ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)maintenance$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to perform maintenance activities - Use /etc/audit/rules.d/maintenance.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/maintenance.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to perform maintenance activities - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to perform maintenance activities - Add watch rule for /var/log/sudo.log in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/log/sudo.log -p wa -k maintenance create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to perform maintenance activities - Check if watch rule for /var/log/sudo.log already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/log/sudo.log\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to perform maintenance activities - Add watch rule for /var/log/sudo.log in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/log/sudo.log -p wa -k maintenance state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - PCI-DSS-Req-10.2.2 - PCI-DSS-Req-10.2.5.b - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_sudo_log_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Access Events to Audit Log Directory The audit system should collect access events to read audit log directory. The following audit rule will assure that access to audit log directory are collected. Set ARCH to either b32 for 32-bit system, or have two lines for both b32 and b64 in case your system is 64-bit. -a always,exit -F arch=ARCH -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset -F key=access-audit-trail If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rule to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rule to /etc/audit/audit.rules file. AU-2(d) AU-12(c) AC-6(9) CM-6(a) 10.3.1 10.3 Attempts to read the logs should be recorded, suspicious access to audit log files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.' # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then OTHER_FILTERS="-F dir=/var/log/audit/ -F perm=r" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="access-audit-trail" SYSCALL_GROUPING="" [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - directory_access_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Access Events to Audit Log Directory - Set architecture for audit tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - directory_access_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Access Events to Audit Log Directory - Perform remediation of Audit rules for /var/log/audit block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access-audit-trail.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access-audit-trail.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32{{ syscalls | join(',') }} -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset -F key=access-audit-trail create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32{{ syscalls | join(',') }} -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset -F key=access-audit-trail create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - directory_access_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Access Events to Audit Log Directory - Perform remediation of Audit rules for /var/log/audit for x86_64 platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access-audit-trail.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access-audit-trail.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64{{ syscalls | join(',') }} -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset -F key=access-audit-trail create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64{{ syscalls | join(',') }} -F dir=/var/log/audit/ -F perm=r -F auid>=1000 -F auid!=unset -F key=access-audit-trail create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - directory_access_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy System Audit Logs Must Have Mode 0750 or Less Permissive If log_group in /etc/audit/auditd.conf is set to a group other than the root group account, change the mode of the audit log files with the following command: $ sudo chmod 0750 /var/log/audit Otherwise, change the mode of the audit log files with the following command: $ sudo chmod 0700 /var/log/audit 1 11 12 13 14 15 16 18 19 3 4 5 6 7 8 APO01.06 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 DSS06.02 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.3.7.3 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 5.2 SR 6.1 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.2 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-004-6 R3.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CIP-007-3 R6.5 CM-6(a) AC-6(1) AU-9 DE.AE-3 DE.AE-5 PR.AC-4 PR.DS-5 PR.PT-1 RS.AN-1 RS.AN-4 SRG-OS-000057-GPOS-00027 SRG-OS-000058-GPOS-00028 SRG-OS-000059-GPOS-00029 6.3.4.1 If users can write to audit logs, audit trails can be modified or destroyed. System Audit Logs Must Be Group Owned By Root All audit logs must be group owned by root user. The path for audit log can be configured via log_file parameter in /etc/audit/auditd.conf or, by default, the path for audit log is /var/log/audit/. To properly set the group owner of /var/log/audit/*, run the command: $ sudo chgrp root /var/log/audit/* If log_group in /etc/audit/auditd.conf is set to a group other than the root group account, change the group ownership of the audit logs to this specific group. 1 11 12 13 14 15 16 18 19 3 4 5 6 7 8 5.4.1.1 APO01.06 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 DSS06.02 MEA02.01 3.3.1 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.3.7.3 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 5.2 SR 6.1 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) AU-9(4) DE.AE-3 DE.AE-5 PR.AC-4 PR.DS-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.5.1 SRG-OS-000057-GPOS-00027 SRG-OS-000058-GPOS-00028 SRG-OS-000059-GPOS-00029 SRG-OS-000206-GPOS-00084 6.3.4.4 10.3.2 10.3 Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if LC_ALL=C grep -iw log_file /etc/audit/auditd.conf; then FILE=$(awk -F "=" '/^log_file/ {print $2}' /etc/audit/auditd.conf | tr -d ' ') else FILE="/var/log/audit/audit.log" fi if LC_ALL=C grep -m 1 -q ^log_group /etc/audit/auditd.conf; then GROUP=$(awk -F "=" '/log_group/ {print $2}' /etc/audit/auditd.conf | tr -d ' ') if ! [ "${GROUP}" == 'root' ]; then chgrp ${GROUP} $FILE* else chgrp root $FILE* fi else chgrp root $FILE* fi else >&2 echo 'Remediation is not applicable, nothing was done' fi Audit Configuration Files Must Be Owned By Group root All audit configuration files must be owned by group root. chown :root /etc/audit/audit*.{rules,conf} /etc/audit/rules.d/* SRG-OS-000063-GPOS-00032 6.3.4.7 Without the capability to restrict which roles and individuals can select which events are audited, unauthorized personnel may be able to prevent the auditing of critical events. Misconfigured audits may degrade the system's performance by overwhelming the audit log. Misconfigured audits may also make it more difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else find -P /etc/audit/ -maxdepth 1 -type f ! -group 0 -regextype posix-extended -regex '^.*audit(\.rules|d\.conf)$' -exec chgrp --no-dereference "$newgroup" {} \; find -P /etc/audit/rules.d/ -maxdepth 1 -type f ! -group 0 -regextype posix-extended -regex '^.*\.rules$' -exec chgrp --no-dereference "$newgroup" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_groupownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupownership_audit_configuration_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupownership_audit_configuration_newgroup: '0' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/audit/ file(s) matching ^.*audit(\.rules|d\.conf)$ ansible.builtin.command: find -P /etc/audit/ -maxdepth 1 -type f ! -group 0 -regextype posix-extended -regex "^.*audit(\.rules|d\.conf)$" register: files_found changed_when: false failed_when: false check_mode: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/audit/ file(s) matching ^.*audit(\.rules|d\.conf)$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ file_groupownership_audit_configuration_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/audit/rules.d/ file(s) matching ^.*\.rules$ ansible.builtin.command: find -P /etc/audit/rules.d/ -maxdepth 1 -type f ! -group 0 -regextype posix-extended -regex "^.*\.rules$" register: files_found changed_when: false failed_when: false check_mode: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /etc/audit/rules.d/ file(s) matching ^.*\.rules$ ansible.builtin.file: path: '{{ item }}' follow: false group: '{{ file_groupownership_audit_configuration_newgroup }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed Audit Configuration Files Must Be Owned By Root All audit configuration files must be owned by root user. To properly set the owner of /etc/audit/, run the command: $ sudo chown root /etc/audit/ To properly set the owner of /etc/audit/rules.d/, run the command: $ sudo chown root /etc/audit/rules.d/ SRG-OS-000063-GPOS-00032 6.3.4.6 Without the capability to restrict which roles and individuals can select which events are audited, unauthorized personnel may be able to prevent the auditing of critical events. Misconfigured audits may degrade the system's performance by overwhelming the audit log. Misconfigured audits may also make it more difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else find -P /etc/audit/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex '^.*audit(\.rules|d\.conf)$' -exec chown --no-dereference "$newown" {} \; find -P /etc/audit/rules.d/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex '^.*\.rules$' -exec chown --no-dereference "$newown" {} \; fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_ownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_ownership_audit_configuration_newown variable if represented by uid ansible.builtin.set_fact: file_ownership_audit_configuration_newown: '0' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/audit/ file(s) matching ^.*audit(\.rules|d\.conf)$ ansible.builtin.command: find -P /etc/audit/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex "^.*audit(\.rules|d\.conf)$" register: files_found changed_when: false failed_when: false check_mode: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/audit/ file(s) matching ^.*audit(\.rules|d\.conf)$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_audit_configuration_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/audit/rules.d/ file(s) matching ^.*\.rules$ ansible.builtin.command: find -P /etc/audit/rules.d/ -maxdepth 1 -type f ! -user 0 -regextype posix-extended -regex "^.*\.rules$" register: files_found changed_when: false failed_when: false check_mode: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /etc/audit/rules.d/ file(s) matching ^.*\.rules$ ansible.builtin.file: path: '{{ item }}' follow: false owner: '{{ file_ownership_audit_configuration_newown }}' state: file with_items: - '{{ files_found.stdout_lines }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed System Audit Logs Must Be Owned By Root All audit logs must be owned by root user and group. By default, the path for audit log is /var/log/audit/. To properly set the owner of /var/log/audit, run the command: $ sudo chown root /var/log/audit To properly set the owner of /var/log/audit/*, run the command: $ sudo chown root /var/log/audit/* 1 11 12 13 14 15 16 18 19 3 4 5 6 7 8 5.4.1.1 APO01.06 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 DSS06.02 MEA02.01 3.3.1 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.3.7.3 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 5.2 SR 6.1 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) AU-9(4) DE.AE-3 DE.AE-5 PR.AC-4 PR.DS-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.5.1 SRG-OS-000057-GPOS-00027 SRG-OS-000058-GPOS-00028 SRG-OS-000059-GPOS-00029 SRG-APP-000118-CTR-000240 10.3.2 10.3 Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if LC_ALL=C grep -m 1 -q ^log_group /etc/audit/auditd.conf; then GROUP=$(awk -F "=" '/log_group/ {print $2}' /etc/audit/auditd.conf | tr -d ' ') if ! [ "${GROUP}" == 'root' ] ; then chown root:${GROUP} /var/log/audit chown root:${GROUP} /var/log/audit/audit.log* else chown root:root /var/log/audit chown root:root /var/log/audit/audit.log* fi else chown root:root /var/log/audit chown root:root /var/log/audit/audit.log* fi else >&2 echo 'Remediation is not applicable, nothing was done' fi System Audit Logs Must Be Owned By Root All audit logs must be owned by root user. The path for audit log can be configured via log_file parameter in /etc/audit/auditd.conf or by default, the path for audit log is /var/log/audit/. To properly set the owner of /var/log/audit/*, run the command: $ sudo chown root /var/log/audit/* 1 11 12 13 14 15 16 18 19 3 4 5 6 7 8 5.4.1.1 APO01.06 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 DSS06.02 MEA02.01 3.3.1 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.3.7.3 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 5.2 SR 6.1 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CM-6(a) AC-6(1) AU-9(4) DE.AE-3 DE.AE-5 PR.AC-4 PR.DS-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.5.1 SRG-OS-000057-GPOS-00027 SRG-OS-000058-GPOS-00028 SRG-OS-000059-GPOS-00029 SRG-OS-000206-GPOS-00084 6.3.4.3 Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if LC_ALL=C grep -iw log_file /etc/audit/auditd.conf; then FILE=$(awk -F "=" '/^log_file/ {print $2}' /etc/audit/auditd.conf | tr -d ' ') chown root $FILE* else chown root /var/log/audit/audit.log* fi else >&2 echo 'Remediation is not applicable, nothing was done' fi Audit Configuration Files Permissions are 640 or More Restrictive All audit configuration files permissions must be 640 or more restrictive. chmod 0640 /etc/audit/audit*.{rules,conf} /etc/audit/rules.d/* AU-12 b SRG-OS-000063-GPOS-00032 6.3.4.5 Without the capability to restrict which roles and individuals can select which events are audited, unauthorized personnel may be able to prevent the auditing of critical events. Misconfigured audits may degrade the system's performance by overwhelming the audit log. Misconfigured audits may also make it more difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then find -P /etc/audit/ -maxdepth 1 -perm /u+xs,g+xws,o+xwrt -type f -regextype posix-extended -regex '^.*audit(\.rules|d\.conf)$' -exec chmod u-xs,g-xws,o-xwrt {} \; find -P /etc/audit/rules.d/ -maxdepth 1 -perm /u+xs,g+xws,o+xwrt -type f -regextype posix-extended -regex '^.*\.rules$' -exec chmod u-xs,g-xws,o-xwrt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12 b - configure_strategy - file_permissions_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/audit/ file(s) ansible.builtin.command: find -P /etc/audit/ -maxdepth 1 -perm /u+xs,g+xws,o+xwrt -type f -regextype posix-extended -regex "^.*audit(\.rules|d\.conf)$" register: files_found changed_when: false failed_when: false check_mode: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12 b - configure_strategy - file_permissions_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/audit/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-xs,g-xws,o-xwrt state: file with_items: - '{{ files_found.stdout_lines }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12 b - configure_strategy - file_permissions_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/audit/rules.d/ file(s) ansible.builtin.command: find -P /etc/audit/rules.d/ -maxdepth 1 -perm /u+xs,g+xws,o+xwrt -type f -regextype posix-extended -regex "^.*\.rules$" register: files_found changed_when: false failed_when: false check_mode: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12 b - configure_strategy - file_permissions_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/audit/rules.d/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-xs,g-xws,o-xwrt state: file with_items: - '{{ files_found.stdout_lines }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12 b - configure_strategy - file_permissions_audit_configuration - low_complexity - low_disruption - medium_severity - no_reboot_needed System Audit Logs Must Have Mode 0640 or Less Permissive If log_group in /etc/audit/auditd.conf is set to a group other than the root group account, change the mode of the audit log files with the following command: $ sudo chmod 0640 audit_file Otherwise, change the mode of the audit log files with the following command: $ sudo chmod 0600 audit_file 1 11 12 13 14 15 16 18 19 3 4 5 6 7 8 5.4.1.1 APO01.06 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 DSS06.02 MEA02.01 3.3.1 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.3.7.3 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.1 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 5.2 SR 6.1 A.10.1.1 A.11.1.4 A.11.1.5 A.11.2.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.1.3 A.13.2.1 A.13.2.3 A.13.2.4 A.14.1.2 A.14.1.3 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.2 A.7.1.1 A.7.1.2 A.7.3.1 A.8.2.2 A.8.2.3 A.9.1.1 A.9.1.2 A.9.2.3 A.9.4.1 A.9.4.4 A.9.4.5 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.3 CIP-007-3 R2.1 CIP-007-3 R2.2 CIP-007-3 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.1 CIP-007-3 R5.1.2 CM-6(a) AC-6(1) AU-9(4) DE.AE-3 DE.AE-5 PR.AC-4 PR.DS-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.5 SRG-OS-000057-GPOS-00027 SRG-OS-000058-GPOS-00028 SRG-OS-000059-GPOS-00029 SRG-OS-000206-GPOS-00084 SRG-APP-000118-CTR-000240 6.3.4.2 10.3.1 10.3 If users can write to audit logs, audit trails can be modified or destroyed. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if LC_ALL=C grep -iw ^log_file /etc/audit/auditd.conf; then FILE=$(awk -F "=" '/^log_file/ {print $2}' /etc/audit/auditd.conf | tr -d ' ') else FILE="/var/log/audit/audit.log" fi if LC_ALL=C grep -m 1 -q ^log_group /etc/audit/auditd.conf; then GROUP=$(awk -F "=" '/log_group/ {print $2}' /etc/audit/auditd.conf | tr -d ' ') if ! [ "${GROUP}" == 'root' ] ; then chmod 0640 $FILE chmod 0440 $FILE.* else chmod 0600 $FILE chmod 0400 $FILE.* fi else chmod 0600 $FILE chmod 0400 $FILE.* fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Get audit log files ansible.builtin.command: grep -iw ^log_file /etc/audit/auditd.conf failed_when: false register: log_file_exists when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Parse log file line ansible.builtin.command: awk -F '=' '/^log_file/ {print $2}' /etc/audit/auditd.conf register: log_file_line when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - log_file_exists is not skipped and (log_file_exists.stdout | length > 0) tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set default log_file if not set ansible.builtin.set_fact: log_file: /var/log/audit/audit.log when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - (log_file_exists is skipped) or (log_file_exists is undefined) or (log_file_exists.stdout | length == 0) tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set log_file from log_file_line if not set already ansible.builtin.set_fact: log_file: '{{ log_file_line.stdout | trim }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - (log_file_exists is not skipped) and (log_file_line.stdout is defined) and (log_file_line.stdout | length > 0) tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Get log files group ansible.builtin.command: grep -m 1 ^log_group /etc/audit/auditd.conf failed_when: false register: log_group_line when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Parse log group line ansible.builtin.command: awk -F '=' '/log_group/ {print $2}' /etc/audit/auditd.conf register: log_group when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - (log_group_line is not skipped) and (log_group_line.stdout | length > 0) tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Apply mode to log file when group root ansible.builtin.file: path: '{{ log_file }}' mode: (( log_group is defined ) and ( ( log_group.stdout | trim ) == 'root' )) | ternary( '0600', '0640') failed_when: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - log_group is not skipped tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: List all log file backups ansible.builtin.find: path: '{{ log_file | dirname }}' patterns: '{{ log_file | basename }}.*' register: backup_files when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Apply mode to log file when group not root ansible.builtin.file: path: '{{ item }}' mode: (( log_group is defined ) and ( ( log_group.stdout | trim ) == 'root' )) | ternary( '0400', '0440') loop: '{{ backup_files.files| map(attribute=''path'') | list }}' failed_when: false when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - backup_files is not skipped tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AC-6(1) - NIST-800-53-AU-9(4) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.1 - file_permissions_var_log_audit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Events that Modify the System's Discretionary Access Controls At a minimum, the audit system should collect file permission changes for all users and root. Note that the "-F arch=b32" lines should be present even on a 64 bit system. These commands identify system calls for auditing. Even if the system is 64 bit it can still execute 32 bit system calls. Additionally, these rules can be configured in a number of ways while still achieving the desired effect. An example of this is that the "-S" calls could be split up and placed on separate lines, however, this is less efficient. Add the following to /etc/audit/audit.rules: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod -a always,exit -F arch=b32 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -F key=perm_mod -a always,exit -F arch=b32 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If your system is 64 bit then these lines should be duplicated and the arch=b32 replaced with arch=b64 as follows: -a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod -a always,exit -F arch=b64 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -F key=perm_mod -a always,exit -F arch=b64 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod Record Events that Modify the System's Discretionary Access Controls - chmod At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="chmod" KEY="perm_mod" SYSCALL_GROUPING="chmod fchmod fchmodat" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit chmod tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chmod for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of chmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of chmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chmod for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of chmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of chmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - chown At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-OS-000474-GPOS-00219 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="chown" KEY="perm_mod" SYSCALL_GROUPING="chown fchown fchownat lchown" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit chown tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chown for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chown for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - fchmod At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchmod" KEY="perm_mod" SYSCALL_GROUPING="chmod fchmod fchmodat" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchmod tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmod for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmod for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - fchmodat At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchmodat" KEY="perm_mod" SYSCALL_GROUPING="chmod fchmod fchmodat" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchmodat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmodat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmodat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmodat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - name: Check existence of fchmodat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - fchmodat2 At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchmodat2 -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchmodat2 -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchmodat2 -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchmodat2 -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchmodat2" KEY="perm_mod" SYSCALL_GROUPING="chmod fchmod fchmodat fchmodat2" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchmodat2 tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat2 for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat2 syscall_grouping: - chmod - fchmod - fchmodat - fchmodat2 - name: Check existence of fchmodat2 in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat2 syscall_grouping: - chmod - fchmod - fchmodat - fchmodat2 - name: Check existence of fchmodat2 in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat2 for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat2 syscall_grouping: - chmod - fchmod - fchmodat - fchmodat2 - name: Check existence of fchmodat2 in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat2 syscall_grouping: - chmod - fchmod - fchmodat - fchmodat2 - name: Check existence of fchmodat2 in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchmodat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - fchown At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-OS-000474-GPOS-00219 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchown" KEY="perm_mod" SYSCALL_GROUPING="chown fchown fchownat lchown" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchown tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchown for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchown for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - fchownat At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-OS-000474-GPOS-00219 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchownat" KEY="perm_mod" SYSCALL_GROUPING="chown fchown fchownat lchown" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchownat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchownat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchownat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - fremovexattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000462-GPOS-00206 SRG-OS-000463-GPOS-00207 SRG-OS-000471-GPOS-00215 SRG-OS-000474-GPOS-00219 SRG-OS-000466-GPOS-00210 SRG-OS-000468-GPOS-00212 SRG-OS-000064-GPOS-00033 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fremovexattr" KEY="perm_mod" SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fremovexattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fremovexattr for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fremovexattr for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - fsetxattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000462-GPOS-00206 SRG-OS-000463-GPOS-00207 SRG-OS-000466-GPOS-00210 SRG-OS-000468-GPOS-00212 SRG-OS-000471-GPOS-00215 SRG-OS-000474-GPOS-00219 SRG-OS-000064-GPOS-00033 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fsetxattr" KEY="perm_mod" SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fsetxattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fsetxattr for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fsetxattr for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - lchown At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000466-GPOS-00210 SRG-OS-000458-GPOS-00203 SRG-OS-000474-GPOS-00219 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="lchown" KEY="perm_mod" SYSCALL_GROUPING="chown fchown fchownat lchown" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit lchown tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lchown for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lchown for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - lremovexattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000462-GPOS-00206 SRG-OS-000463-GPOS-00207 SRG-OS-000468-GPOS-00212 SRG-OS-000471-GPOS-00215 SRG-OS-000474-GPOS-00219 SRG-OS-000466-GPOS-00210 SRG-OS-000064-GPOS-00033 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="lremovexattr" KEY="perm_mod" SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit lremovexattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lremovexattr for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lremovexattr for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - lsetxattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000462-GPOS-00206 SRG-OS-000463-GPOS-00207 SRG-OS-000466-GPOS-00210 SRG-OS-000468-GPOS-00212 SRG-OS-000471-GPOS-00215 SRG-OS-000474-GPOS-00219 SRG-OS-000064-GPOS-00033 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="lsetxattr" KEY="perm_mod" SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit lsetxattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lsetxattr for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lsetxattr for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - removexattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000462-GPOS-00206 SRG-OS-000463-GPOS-00207 SRG-OS-000468-GPOS-00212 SRG-OS-000471-GPOS-00215 SRG-OS-000474-GPOS-00219 SRG-OS-000466-GPOS-00210 SRG-OS-000064-GPOS-00033 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="removexattr" KEY="perm_mod" SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit removexattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for removexattr for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of removexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of removexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for removexattr for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of removexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of removexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - setxattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.5.5 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000466-GPOS-00210 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-APP-000091-CTR-000160 SRG-APP-000492-CTR-001220 SRG-APP-000493-CTR-001225 SRG-APP-000494-CTR-001230 SRG-APP-000500-CTR-001260 SRG-APP-000507-CTR-001295 SRG-APP-000495-CTR-001235 R73 6.3.3.18 10.3.4 10.3 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="setxattr" KEY="perm_mod" SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit setxattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for setxattr for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for setxattr for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - fremovexattr - lremovexattr - removexattr - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.5 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.4 - audit_rules_dac_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - umount At a minimum, the audit system should collect file system umount changes. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S umount -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S umount -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. SRG-OS-000037-GPOS-00015 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then ACTION_ARCH_FILTERS="-a always,exit -F arch=b32" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="umount" KEY="perm_mod" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_dac_modification_umount - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for umount for x86 platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - umount syscall_grouping: [] - name: Check existence of umount in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - umount syscall_grouping: [] - name: Check existence of umount in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - audit_rules_dac_modification_umount - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Events that Modify the System's Discretionary Access Controls - umount2 At a minimum, the audit system should collect file system umount2 changes. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S umount2 -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S umount2 -F auid>=1000 -F auid!=unset -F key=perm_mod If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S umount2 -F auid>=1000 -F auid!=unset -F key=perm_mod If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S umount2 -F auid>=1000 -F auid!=unset -F key=perm_mod Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. SRG-OS-000037-GPOS-00015 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 R73 The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="umount2" KEY="perm_mod" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_dac_modification_umount2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit umount2 tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - audit_rules_dac_modification_umount2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for umount2 for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - umount2 syscall_grouping: [] - name: Check existence of umount2 in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - umount2 syscall_grouping: [] - name: Check existence of umount2 in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_dac_modification_umount2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for umount2 for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - umount2 syscall_grouping: [] - name: Check existence of umount2 in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - umount2 syscall_grouping: [] - name: Check existence of umount2 in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=perm_mod create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - audit_rules_dac_modification_umount2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Execution Attempts to Run ACL Privileged Commands At a minimum, the audit system should collect the execution of ACL privileged commands for all users and root. Record Any Attempts to Run chacl At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 6.3.3.26 Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter). # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/chacl -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_execution_chacl - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run chacl - Perform remediation of Audit rules for /usr/bin/chacl block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_execution_chacl - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Any Attempts to Run setfacl At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 6.3.3.25 Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter). # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/setfacl -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_execution_setfacl - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run setfacl - Perform remediation of Audit rules for /usr/bin/setfacl block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_execution_setfacl - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Execution Attempts to Run SELinux Privileged Commands At a minimum, the audit system should collect the execution of SELinux privileged commands for all users and root. Record Any Attempts to Run chcon At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000468-GPOS-00212 SRG-OS-000471-GPOS-00215 SRG-OS-000463-GPOS-00207 SRG-OS-000465-GPOS-00209 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 6.3.3.24 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/chcon -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_chcon - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run chcon - Perform remediation of Audit rules for /usr/bin/chcon block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_chcon - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Any Attempts to Run restorecon At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000392-GPOS-00172 SRG-OS-000463-GPOS-00207 SRG-OS-000465-GPOS-00209 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/restorecon -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_restorecon - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run restorecon - Perform remediation of Audit rules for /usr/sbin/restorecon block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_restorecon - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Any Attempts to Run semanage At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000463-GPOS-00207 SRG-OS-000465-GPOS-00209 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/semanage -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_semanage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run semanage - Perform remediation of Audit rules for /usr/sbin/semanage block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_semanage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Any Attempts to Run setfiles At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-2(d) AU-12(c) AC-6(9) CM-6(a) SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000463-GPOS-00207 SRG-OS-000465-GPOS-00209 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/setfiles -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_setfiles - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run setfiles - Perform remediation of Audit rules for /usr/sbin/setfiles block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_setfiles - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Any Attempts to Run setsebool At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000463-GPOS-00207 SRG-OS-000465-GPOS-00209 SRG-APP-000495-CTR-001235 SRG-APP-000496-CTR-001240 SRG-APP-000497-CTR-001245 SRG-APP-000498-CTR-001250 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/setsebool -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_setsebool - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run setsebool - Perform remediation of Audit rules for /usr/sbin/setsebool block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_setsebool - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Any Attempts to Run seunshare At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-2(d) AU-12(c) AC-6(9) CM-6(a) Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/seunshare -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_seunshare - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run seunshare - Perform remediation of Audit rules for /usr/sbin/seunshare block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_execution_seunshare - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record File Deletion Events by User At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat,renameat2 -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat,renameat2 -F auid>=1000 -F auid!=unset -F key=delete Ensure auditd Collects File Deletion Events by User At a minimum the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat,renameat2 -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat2 -S renameat -F auid>=1000 -F auid!=unset -F key=delete This rule checks for multiple syscalls related to file deletion; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: audit_rules_file_deletion_events_rmdiraudit_rules_file_deletion_events_unlinkaudit_rules_file_deletion_events_unlinkataudit_rules_file_deletion_events_renameaudit_rules_file_deletion_events_renameataudit_rules_file_deletion_events_renameat2 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence. Ensure auditd Collects File Deletion Events by User - rename At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.4 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.1.1 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.MA-2 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-OS-000467-GPOS-00211 SRG-OS-000468-GPOS-00212 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.22 10.2.1.7 10.2.1 10.2 Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="rename" KEY="delete" SYSCALL_GROUPING="unlink unlinkat rename renameat renameat2 rmdir" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit rename tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rename for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rename in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rename in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rename for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rename in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rename in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Ensure auditd Collects File Deletion Events by User - renameat At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.4 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.1.1 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.MA-2 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-OS-000467-GPOS-00211 SRG-OS-000468-GPOS-00212 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.22 10.2.1.7 10.2.1 10.2 Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="renameat" KEY="delete" SYSCALL_GROUPING="unlink unlinkat rename renameat renameat2 rmdir" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit renameat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Ensure auditd Collects File Deletion Events by User - renameat2 At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S renameat2 -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S renameat2 -F auid>=1000 -F auid!=unset -F key=delete 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-OS-000467-GPOS-00211 SRG-OS-000468-GPOS-00212 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.22 10.2.1.7 10.2.1 10.2 Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="renameat2" KEY="delete" SYSCALL_GROUPING="unlink unlinkat rename renameat renameat2 rmdir" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit renameat2 tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat2 for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat2 syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat2 in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat2 syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat2 in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat2 for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat2 syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat2 in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat2 syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of renameat2 in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_renameat2 - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Ensure auditd Collects File Deletion Events by User - rmdir At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.4 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.1.1 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.MA-2 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-OS-000467-GPOS-00211 SRG-OS-000468-GPOS-00212 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 10.2.1.7 10.2.1 10.2 Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="rmdir" KEY="delete" SYSCALL_GROUPING="unlink unlinkat rename renameat renameat2 rmdir" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rmdir - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit rmdir tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rmdir - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rmdir for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rmdir syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rmdir in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rmdir syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rmdir in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rmdir - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rmdir for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rmdir syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rmdir in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rmdir syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of rmdir in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_rmdir - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Ensure auditd Collects File Deletion Events by User - unlink At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S unlink -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S unlink -F auid>=1000 -F auid!=unset -F key=delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.4 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.1.1 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.MA-2 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-OS-000467-GPOS-00211 SRG-OS-000468-GPOS-00212 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.22 10.2.1.7 10.2.1 10.2 Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="unlink" KEY="delete" SYSCALL_GROUPING="unlink unlinkat rename renameat renameat2 rmdir" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit unlink tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlink for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlink in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlink in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlink for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlink in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlink in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Ensure auditd Collects File Deletion Events by User - unlinkat At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.5 4.3.3.6.6 4.3.3.6.7 4.3.3.6.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.4 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.1.1 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.MA-2 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-OS-000467-GPOS-00211 SRG-OS-000468-GPOS-00212 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 R73 6.3.3.22 10.2.1.7 10.2.1 10.2 Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="unlinkat" KEY="delete" SYSCALL_GROUPING="unlink unlinkat rename renameat renameat2 rmdir" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit unlinkat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlinkat for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlinkat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlinkat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlinkat for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlinkat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/delete.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - unlink - unlinkat - rename - renameat - renameat2 - rmdir - name: Check existence of unlinkat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=delete create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.7 - audit_rules_file_deletion_events_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unauthorized Access Attempts Events to Files (unsuccessful) At a minimum, the audit system should collect unauthorized file accesses for all users and root. Note that the "-F arch=b32" lines should be present even on a 64 bit system. These commands identify system calls for auditing. Even if the system is 64 bit it can still execute 32 bit system calls. Additionally, these rules can be configured in a number of ways while still achieving the desired effect. An example of this is that the "-S" calls could be split up and placed on separate lines, however, this is less efficient. Add the following to /etc/audit/audit.rules: -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If your system is 64 bit then these lines should be duplicated and the arch=b32 replaced with arch=b64 as follows: -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access Record Successful Permission Changes to Files - chmod At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S chmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S chmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S chmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S chmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Ownership Changes to Files - chown At a minimum, the audit system should collect file ownership changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S chown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S chown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S chown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S chown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File ownership attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Access Attempts to Files - creat At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S creat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S creat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S creat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S creat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File access attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - fchmod At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchmod -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - fchmodat At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchmodat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchmodat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchmodat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchmodat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Ownership Changes to Files - fchown At a minimum, the audit system should collect file ownership changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File ownership attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Ownership Changes to Files - fchownat At a minimum, the audit system should collect file ownership changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fchownat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchownat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fchownat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchownat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File ownership attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - fremovexattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - fsetxattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S fsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S fsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Access Attempts to Files - ftruncate At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S ftruncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S ftruncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S ftruncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S ftruncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File access attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Ownership Changes to Files - lchown At a minimum, the audit system should collect file ownership changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S lchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S lchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lchown -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File ownership attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - lremovexattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S lremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S lremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lremovexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - lsetxattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S lsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S lsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lsetxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Access Attempts to Files - open At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File access attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Access Attempts to Files - open_by_handle_at At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File access attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Creation Attempts to Files - open_by_handle_at O_CREAT The open_by_handle_at syscall can be used to create new files when O_CREAT flag is specified. The following audit rules will assure that successful attempts to create a file via open_by_handle_at syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open_by_handle_at -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open_by_handle_at,open_by_handle_at -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create Successful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Creation Attempts to Files - open_by_handle_at O_TRUNC_WRITE The audit system should collect detailed file access records for all users and root. The open_by_handle_at syscall can be used to modify files if called for write operation with the O_TRUNC_WRITE flag. The following audit rules will assure that successful attempts to create a file via open_by_handle_at syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open_by_handle_at -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open,open_by_handle_at -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification Successful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Creation Attempts to Files - open O_CREAT The open syscall can be used to create new files when O_CREAT flag is specified. The following audit rules will assure that successful attempts to create a file via open syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open,open -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create Successful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Creation Attempts to Files - open O_TRUNC_WRITE The audit system should collect detailed file access records for all users and root. The open syscall can be used to modify files if called for write operation with the O_TRUNC_WRITE flag. The following audit rules will assure that successful attempts to create a file via open syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open,openat -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification Successful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Access Attempts to Files - openat At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S openat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S openat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File access attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Creation Attempts to Files - openat O_CREAT The openat syscall can be used to create new files when O_CREAT flag is specified. The following audit rules will assure that successful attempts to create a file via openat syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S openat -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-create Successful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Creation Attempts to Files - openat O_TRUNC_WRITE The audit system should collect detailed file access records for all users and root. The openat syscall can be used to modify files if called for write operation with the O_TRUNC_WRITE flag. The following audit rules will assure that successful attempts to create a file via openat syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S openat -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open,openat -F a2&01003 -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-modification Successful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - removexattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S removexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S removexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S removexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S removexattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File permission changes could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Delete Attempts to Files - rename At a minimum, the audit system should collect file deletion for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S rename -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S rename -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S rename -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S rename -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File deletion attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Delete Attempts to Files - renameat At a minimum, the audit system should collect file deletion for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S renameat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S renameat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S renameat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S renameat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File deletion attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Permission Changes to Files - setxattr At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S setxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S setxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S setxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S setxattr -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File deletion attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Access Attempts to Files - truncate At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S truncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S truncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S truncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S truncate -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File access attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Delete Attempts to Files - unlink At a minimum, the audit system should collect file deletion for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S unlink -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S unlink -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S unlink -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S unlink -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File deletion attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Successful Delete Attempts to Files - unlinkat At a minimum, the audit system should collect file deletion for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S unlinkat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S unlinkat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S unlinkat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S unlinkat -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. File deletion attempts could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Ensure auditd Collects Unauthorized Access Attempts to Files (unsuccessful) At a minimum the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access This rule checks for multiple syscalls related to unsuccessful file modification; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: audit_rules_unsuccessful_file_modification_openaudit_rules_unsuccessful_file_modification_ftruncateaudit_rules_unsuccessful_file_modification_creat 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 0582 0584 05885 0586 0846 0957 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. Record Unsuccessful Permission Changes to Files - chmod The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S chmod -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S chmod -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S chmod -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S chmod -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="chmod" KEY="access" SYSCALL_GROUPING="chmod fchmod fchmodat fsetxattr lsetxattr setxattr" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit chmod tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chmod EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chmod EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chmod EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chmod EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of chmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Ownership Changes to Files - chown The audit system should collect unsuccessful file ownership change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S chown -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S chown -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S chown -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S chown -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S lchown,fchown,chown,fchownat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change ownership of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="chown" KEY="access" SYSCALL_GROUPING="chown fchown fchownat lchown" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit chown tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chown EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chown EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chown EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for chown EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - chown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of chown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_chown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Access Attempts to Files - creat At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-APP-000495-CTR-001235 R73 6.3.3.11 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="creat" KEY="access" SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_creat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit creat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_creat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for creat EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_creat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for creat EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_creat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for creat EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_creat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for creat EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - creat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of creat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_creat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Permission Changes to Files - fchmod The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S fchmod -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S fchmod -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchmod -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S fchmod -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchmod" KEY="access" SYSCALL_GROUPING="chmod fchmod fchmodat fsetxattr lsetxattr setxattr" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchmod tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmod EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmod EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmod EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmod EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmod syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmod in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmod - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Permission Changes to Files - fchmodat The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S fchmodat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S fchmodat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchmodat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S fchmodat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchmodat" KEY="access" SYSCALL_GROUPING="chmod fchmod fchmodat fsetxattr lsetxattr setxattr" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchmodat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchmodat EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchmodat syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fchmodat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchmodat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Ownership Changes to Files - fchown The audit system should collect unsuccessful file ownership change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S fchown -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S fchown -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchown -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S fchown -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S lchown,fchown,chown,fchownat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change ownership of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchown" KEY="access" SYSCALL_GROUPING="chown fchown fchownat lchown" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchown tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchown EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchown EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchown EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchown EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Ownership Changes to Files - fchownat The audit system should collect unsuccessful file ownership change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S fchownat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S fchownat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fchownat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S fchownat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S lchown,fchown,chown,fchownat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change ownership of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fchownat" KEY="access" SYSCALL_GROUPING="chown fchown fchownat lchown" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fchownat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchownat EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchownat EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchownat EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fchownat EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fchownat syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of fchownat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fchownat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Permission Changes to Files - fremovexattr The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S fremovexattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S fremovexattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fremovexattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S fremovexattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fremovexattr" KEY="access" SYSCALL_GROUPING="" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fremovexattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fremovexattr EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fremovexattr EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fremovexattr EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fremovexattr EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fremovexattr syscall_grouping: [] - name: Check existence of fremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Permission Changes to Files - fsetxattr The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S fsetxattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S fsetxattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="fsetxattr" KEY="access" SYSCALL_GROUPING="chmod fchmod fchmodat fsetxattr lsetxattr setxattr" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit fsetxattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fsetxattr EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fsetxattr EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fsetxattr EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for fsetxattr EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - fsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of fsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_fsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Access Attempts to Files - ftruncate At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-APP-000495-CTR-001235 R73 6.3.3.11 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="ftruncate" KEY="access" SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_ftruncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit ftruncate tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_ftruncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for ftruncate EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_ftruncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for ftruncate EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_ftruncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for ftruncate EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_ftruncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for ftruncate EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - ftruncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of ftruncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_ftruncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Ownership Changes to Files - lchown The audit system should collect unsuccessful file ownership change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S lchown -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S lchown -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lchown -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S lchown -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S lchown,fchown,chown,fchownat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change ownership of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="lchown" KEY="access" SYSCALL_GROUPING="chown fchown fchownat lchown" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit lchown tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lchown EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lchown EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lchown EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lchown EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lchown syscall_grouping: - chown - fchown - fchownat - lchown - name: Check existence of lchown in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lchown - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Permission Changes to Files - lremovexattr The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S lremovexattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S lremovexattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lremovexattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S lremovexattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="lremovexattr" KEY="access" SYSCALL_GROUPING="" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit lremovexattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lremovexattr EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lremovexattr EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lremovexattr EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lremovexattr EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lremovexattr syscall_grouping: [] - name: Check existence of lremovexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lremovexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Permission Changes to Files - lsetxattr The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S lsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S lsetxattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S lsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S lsetxattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="lsetxattr" KEY="access" SYSCALL_GROUPING="chmod fchmod fchmodat fsetxattr lsetxattr setxattr" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit lsetxattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lsetxattr EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lsetxattr EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lsetxattr EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for lsetxattr EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - lsetxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of lsetxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_lsetxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Access Attempts to Files - open At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-APP-000495-CTR-001235 R73 6.3.3.11 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open" KEY="access" SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Access Attempts to Files - open_by_handle_at At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-APP-000495-CTR-001235 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="open_by_handle_at" KEY="access" SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit open_by_handle_at tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for open_by_handle_at EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - open_by_handle_at syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of open_by_handle_at in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Creation Attempts to Files - open_by_handle_at O_CREAT The audit system should collect unauthorized file accesses for all users and root. The open_by_handle_at syscall can be used to create new files when O_CREAT flag is specified. The following auidt rules will asure that unsuccessful attempts to create a file via open_by_handle_at syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at_o_creat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Add unsuccessful file operations audit rules ansible.builtin.blockinfile: path: /etc/audit/rules.d/30-ospp-v42-remediation.rules create: true block: |- ## This content is a section of an Audit config snapshot recommended for Fedora systems that target OSPP compliance. ## The following content has been retreived on 2019-03-11 from: https://github.com/linux-audit/audit-userspace/blob/master/rules/30-ospp-v42.rules ## The purpose of these rules is to meet the requirements for Operating ## System Protection Profile (OSPP)v4.2. These rules depends on having ## 10-base-config.rules, 11-loginuid.rules, and 43-module-load.rules installed. ## Unsuccessful file creation (open with O_CREAT) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create ## Unsuccessful file modifications (open for write or truncate) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification ## Unsuccessful file access (any other opens) This has to go last. -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at_o_creat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Unsuccessful Modification Attempts to Files - open_by_handle_at O_TRUNC_WRITE The audit system should collect detailed unauthorized file accesses for all users and root. The open_by_handle_at syscall can be used to modify files if called for write operation of with O_TRUNC_WRITE flag. The following auidt rules will asure that unsuccessful attempts to modify a file via open_by_handle_at syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at_o_trunc_write - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Add unsuccessful file operations audit rules ansible.builtin.blockinfile: path: /etc/audit/rules.d/30-ospp-v42-remediation.rules create: true block: |- ## This content is a section of an Audit config snapshot recommended for Fedora systems that target OSPP compliance. ## The following content has been retreived on 2019-03-11 from: https://github.com/linux-audit/audit-userspace/blob/master/rules/30-ospp-v42.rules ## The purpose of these rules is to meet the requirements for Operating ## System Protection Profile (OSPP)v4.2. These rules depends on having ## 10-base-config.rules, 11-loginuid.rules, and 43-module-load.rules installed. ## Unsuccessful file creation (open with O_CREAT) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create ## Unsuccessful file modifications (open for write or truncate) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification ## Unsuccessful file access (any other opens) This has to go last. -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_by_handle_at_o_trunc_write - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Unauthorized Access Attempts To open_by_handle_at Are Ordered Correctly The audit system should collect detailed unauthorized file accesses for all users and root. To correctly identify unsuccessful creation, unsuccessful modification and unsuccessful access of files via open_by_handle_at syscall the audit rules collecting these events need to be in certain order. The more specific rules need to come before the less specific rules. The reason for that is that more specific rules cover a subset of events covered in the less specific rules, thus, they need to come before to not be overshadowed by less specific rules, which match a bigger set of events. Make sure that rules for unsuccessful calls of open_by_handle_at syscall are in the order shown below. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), check the order of rules below in a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, check the order of rules below in /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 The more specific rules cover a subset of events covered by the less specific rules. By ordering them from more specific to less specific, it is assured that the less specific rule will not catch events better recorded by the more specific rule. Record Unsuccessful Creation Attempts to Files - open O_CREAT The audit system should collect unauthorized file accesses for all users and root. The open syscall can be used to create new files when O_CREAT flag is specified. The following auidt rules will asure that unsuccessful attempts to create a file via open syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_o_creat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Add unsuccessful file operations audit rules ansible.builtin.blockinfile: path: /etc/audit/rules.d/30-ospp-v42-remediation.rules create: true block: |- ## This content is a section of an Audit config snapshot recommended for Fedora systems that target OSPP compliance. ## The following content has been retreived on 2019-03-11 from: https://github.com/linux-audit/audit-userspace/blob/master/rules/30-ospp-v42.rules ## The purpose of these rules is to meet the requirements for Operating ## System Protection Profile (OSPP)v4.2. These rules depends on having ## 10-base-config.rules, 11-loginuid.rules, and 43-module-load.rules installed. ## Unsuccessful file creation (open with O_CREAT) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create ## Unsuccessful file modifications (open for write or truncate) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification ## Unsuccessful file access (any other opens) This has to go last. -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_o_creat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Unsuccessful Modification Attempts to Files - open O_TRUNC_WRITE The audit system should collect detailed unauthorized file accesses for all users and root. The open syscall can be used to modify files if called for write operation of with O_TRUNC_WRITE flag. The following auidt rules will asure that unsuccessful attempts to modify a file via open syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_o_trunc_write - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Add unsuccessful file operations audit rules ansible.builtin.blockinfile: path: /etc/audit/rules.d/30-ospp-v42-remediation.rules create: true block: |- ## This content is a section of an Audit config snapshot recommended for Fedora systems that target OSPP compliance. ## The following content has been retreived on 2019-03-11 from: https://github.com/linux-audit/audit-userspace/blob/master/rules/30-ospp-v42.rules ## The purpose of these rules is to meet the requirements for Operating ## System Protection Profile (OSPP)v4.2. These rules depends on having ## 10-base-config.rules, 11-loginuid.rules, and 43-module-load.rules installed. ## Unsuccessful file creation (open with O_CREAT) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create ## Unsuccessful file modifications (open for write or truncate) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification ## Unsuccessful file access (any other opens) This has to go last. -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_open_o_trunc_write - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Rules For Unauthorized Attempts To open Are Ordered Correctly The audit system should collect detailed unauthorized file accesses for all users and root. To correctly identify unsuccessful creation, unsuccessful modification and unsuccessful access of files via open syscall the audit rules collecting these events need to be in certain order. The more specific rules need to come before the less specific rules. The reason for that is that more specific rules cover a subset of events covered in the less specific rules, thus, they need to come before to not be overshadowed by less specific rules, which match a bigger set of events. Make sure that rules for unsuccessful calls of open syscall are in the order shown below. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), check the order of rules below in a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, check the order of rules below in /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 The more specific rules cover a subset of events covered by the less specific rules. By ordering them from more specific to less specific, it is assured that the less specific rule will not catch events better recorded by the more specific rule. Record Unsuccessful Access Attempts to Files - openat At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-APP-000495-CTR-001235 R73 6.3.3.11 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="openat" KEY="access" SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit openat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for openat EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - openat syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of openat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Creation Attempts to Files - openat O_CREAT The audit system should collect unauthorized file accesses for all users and root. The openat syscall can be used to create new files when O_CREAT flag is specified. The following auidt rules will asure that unsuccessful attempts to create a file via openat syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S openat -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat_o_creat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Add unsuccessful file operations audit rules ansible.builtin.blockinfile: path: /etc/audit/rules.d/30-ospp-v42-remediation.rules create: true block: |- ## This content is a section of an Audit config snapshot recommended for Fedora systems that target OSPP compliance. ## The following content has been retreived on 2019-03-11 from: https://github.com/linux-audit/audit-userspace/blob/master/rules/30-ospp-v42.rules ## The purpose of these rules is to meet the requirements for Operating ## System Protection Profile (OSPP)v4.2. These rules depends on having ## 10-base-config.rules, 11-loginuid.rules, and 43-module-load.rules installed. ## Unsuccessful file creation (open with O_CREAT) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create ## Unsuccessful file modifications (open for write or truncate) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification ## Unsuccessful file access (any other opens) This has to go last. -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat_o_creat - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Unsuccessful Modification Attempts to Files - openat O_TRUNC_WRITE The audit system should collect detailed unauthorized file accesses for all users and root. The openat syscall can be used to modify files if called for write operation of with O_TRUNC_WRITE flag. The following auidt rules will asure that unsuccessful attempts to modify a file via openat syscall are collected. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the rules below to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the rules below to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S openat -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat_o_trunc_write - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Add unsuccessful file operations audit rules ansible.builtin.blockinfile: path: /etc/audit/rules.d/30-ospp-v42-remediation.rules create: true block: |- ## This content is a section of an Audit config snapshot recommended for Fedora systems that target OSPP compliance. ## The following content has been retreived on 2019-03-11 from: https://github.com/linux-audit/audit-userspace/blob/master/rules/30-ospp-v42.rules ## The purpose of these rules is to meet the requirements for Operating ## System Protection Profile (OSPP)v4.2. These rules depends on having ## 10-base-config.rules, 11-loginuid.rules, and 43-module-load.rules installed. ## Unsuccessful file creation (open with O_CREAT) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S open -F a1&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create ## Unsuccessful file modifications (open for write or truncate) -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat,open_by_handle_at -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S open -F a1&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification ## Unsuccessful file access (any other opens) This has to go last. -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_openat_o_trunc_write - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Rules For Unauthorized Attempts To openat Are Ordered Correctly The audit system should collect detailed unauthorized file accesses for all users and root. To correctly identify unsuccessful creation, unsuccessful modification and unsuccessful access of files via openat syscall the audit rules collecting these events need to be in certain order. The more specific rules need to come before the less specific rules. The reason for that is that more specific rules cover a subset of events covered in the less specific rules, thus, they need to come before to not be overshadowed by less specific rules, which match a bigger set of events. Make sure that rules for unsuccessful calls of openat syscall are in the order shown below. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), check the order of rules below in a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, check the order of rules below in /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S openat -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b32 -S openat -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S openat -F a2&0100 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat -F a2&0100 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-create -a always,exit -F arch=b64 -S openat -F a2&01003 -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat -F a2&01003 -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-modification -a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-access -a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-access 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000392-GPOS-00172 The more specific rules cover a subset of events covered by the less specific rules. By ordering them from more specific to less specific, it is assured that the less specific rule will not catch events better recorded by the more specific rule. Record Unsuccessful Permission Changes to Files - removexattr The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S removexattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S removexattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S removexattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S removexattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="removexattr" KEY="access" SYSCALL_GROUPING="" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit removexattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for removexattr EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for removexattr EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for removexattr EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for removexattr EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - removexattr syscall_grouping: [] - name: Check existence of removexattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_removexattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Delete Attempts to Files - rename The audit system should collect unsuccessful file deletion attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S rename -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b32 -S rename -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S rename -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b64 -S rename -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S unlink,unlinkat,rename,renameat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000468-GPOS-00212 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 Unsuccessful attempts to delete files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="rename" KEY="access" SYSCALL_GROUPING="rename renameat unlink unlinkat" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit rename tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rename EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rename EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rename EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for rename EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - rename syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of rename in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_rename - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Delete Attempts to Files - renameat The audit system should collect unsuccessful file deletion attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S renameat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b32 -S renameat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S renameat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b64 -S renameat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S unlink,unlinkat,rename,renameat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000468-GPOS-00212 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 Unsuccessful attempts to delete files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="renameat" KEY="access" SYSCALL_GROUPING="rename renameat unlink unlinkat" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit renameat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for renameat EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - renameat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of renameat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_renameat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Permission Changes to Files - setxattr The audit system should collect unsuccessful file permission change attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S setxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b32 -S setxattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S setxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change -a always,exit -F arch=b64 -S setxattr -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the audit rule checks a system call independently of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat,setxattr,lsetxattr,fsetxattr -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-perm-change AU-2(d) AU-12(c) CM-6(a) Unsuccessful attempts to change permissions of files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="setxattr" KEY="access" SYSCALL_GROUPING="chmod fchmod fchmodat fsetxattr lsetxattr setxattr" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit setxattr tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for setxattr EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for setxattr EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for setxattr EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for setxattr EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - setxattr syscall_grouping: - chmod - fchmod - fchmodat - fsetxattr - lsetxattr - setxattr - name: Check existence of setxattr in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_unsuccessful_file_modification_setxattr - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Access Attempts to Files - truncate At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access -a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000064-GPOS-00033 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-APP-000495-CTR-001235 R73 6.3.3.11 Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="truncate" KEY="access" SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_truncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit truncate tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_truncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for truncate EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_truncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for truncate EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_truncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for truncate EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_truncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for truncate EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - truncate syscall_grouping: - creat - ftruncate - truncate - open - openat - open_by_handle_at - name: Check existence of truncate in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_truncate - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Delete Attempts to Files - unlink The audit system should collect unsuccessful file deletion attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S unlink -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b32 -S unlink -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S unlink -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b64 -S unlink -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S unlink,unlinkat,rename,renameat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000468-GPOS-00212 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 Unsuccessful attempts to delete files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="unlink" KEY="access" SYSCALL_GROUPING="rename renameat unlink unlinkat" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit unlink tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlink EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlink EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlink EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlink EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlink syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlink in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlink - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Unsuccessful Delete Attempts to Files - unlinkat The audit system should collect unsuccessful file deletion attempts for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file. -a always,exit -F arch=b32 -S unlinkat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b32 -S unlinkat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete If the system is 64 bit then also add the following lines: -a always,exit -F arch=b64 -S unlinkat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete -a always,exit -F arch=b64 -S unlinkat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-delete Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping system calls related to the same event is more efficient. See the following example: -a always,exit -F arch=b32 -S unlink,unlinkat,rename,renameat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccesful-delete 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.4 Req-10.2.1 SRG-OS-000064-GPOS-00033 SRG-OS-000392-GPOS-00172 SRG-OS-000458-GPOS-00203 SRG-OS-000461-GPOS-00205 SRG-OS-000468-GPOS-00212 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 Unsuccessful attempts to delete files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="unlinkat" KEY="access" SYSCALL_GROUPING="rename renameat unlink unlinkat" for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EACCES" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F exit=-EPERM" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit unlinkat tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlinkat EACCES for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlinkat EACCES for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlinkat EPERM for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for unlinkat EPERM for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/access.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - unlinkat syscall_grouping: - rename - renameat - unlink - unlinkat - name: Check existence of unlinkat in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - NIST-800-171-3.1.7 - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.1 - PCI-DSS-Req-10.2.4 - audit_rules_unsuccessful_file_modification_unlinkat - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Record Information on Kernel Modules Loading and Unloading To capture kernel module loading and unloading events, use following lines, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S init_module,delete_module -F key=modules Place to add the lines depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the lines to file /etc/audit/audit.rules. Ensure auditd Collects Information on Kernel Module Loading and Unloading To capture kernel module loading and unloading events, use following lines, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S init_module,finit_module,delete_module -F key=modules The place to add the lines depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the lines to file /etc/audit/audit.rules. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 The addition/removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system # Note: 32-bit and 64-bit kernel syscall numbers not always line up => # it's required on a 64-bit system to check also for the presence # of 32-bit's equivalent of the corresponding rule. # (See `man 7 audit.rules` for details ) [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="" SYSCALL="init_module finit_module delete_module" KEY="modules" SYSCALL_GROUPING="init_module finit_module delete_module" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - audit_rules_kernel_module_loading - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Set architecture for audit tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - audit_rules_kernel_module_loading - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for kernel module loading for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - init_module - delete_module - finit_module syscall_grouping: - init_module - delete_module - finit_module - name: Check existence of init_module, delete_module, finit_module in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modules.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modules.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=modules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - init_module - delete_module - finit_module syscall_grouping: - init_module - delete_module - finit_module - name: Check existence of init_module, delete_module, finit_module in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=modules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - audit_rules_kernel_module_loading - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy - name: Perform remediation of Audit rules for kernel module loading for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - init_module - delete_module - finit_module syscall_grouping: - init_module - delete_module - finit_module - name: Check existence of init_module, delete_module, finit_module in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/modules.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/modules.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=modules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - init_module - delete_module - finit_module syscall_grouping: - init_module - delete_module - finit_module - name: Check existence of init_module, delete_module, finit_module in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=modules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.7 - audit_rules_kernel_module_loading - low_complexity - low_disruption - medium_severity - reboot_required - restrict_strategy Ensure auditd Collects Information on Kernel Module Unloading - create_module To capture kernel module unloading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S create_module -F key=module-change Place to add the line depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the line to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the line to file /etc/audit/audit.rules. SRG-OS-000471-GPOS-00216 SRG-OS-000477-GPOS-00222 6.3.3.31 The removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system # Note: 32-bit and 64-bit kernel syscall numbers not always line up => # it's required on a 64-bit system to check also for the presence # of 32-bit's equivalent of the corresponding rule. # (See `man 7 audit.rules` for details ) [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="" SYSCALL="create_module" KEY="module-change" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_kernel_module_loading_create - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set architecture for audit finit_module tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - audit_rules_kernel_module_loading_create - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Perform remediation of Audit rules for finit_module for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - create_module syscall_grouping: [] - name: Check existence of create_module in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/module-change.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - create_module syscall_grouping: [] - name: Check existence of create_module in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - audit_rules_kernel_module_loading_create - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Perform remediation of Audit rules for finit_module for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - create_module syscall_grouping: [] - name: Check existence of create_module in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/module-change.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - create_module syscall_grouping: [] - name: Check existence of create_module in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - audit_rules_kernel_module_loading_create - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20create_module%20-k%20module-change%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20create_module%20-k%20module-change%0A mode: 0600 path: /etc/audit/rules.d/75-kernel-module-loading-create.rules overwrite: true Ensure auditd Collects Information on Kernel Module Unloading - delete_module To capture kernel module unloading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S delete_module -F key=modules Place to add the line depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the line to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the line to file /etc/audit/audit.rules. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000471-GPOS-00216 SRG-OS-000477-GPOS-00222 SRG-APP-000495-CTR-001235 SRG-APP-000504-CTR-001280 R73 6.3.3.30 The removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system # Note: 32-bit and 64-bit kernel syscall numbers not always line up => # it's required on a 64-bit system to check also for the presence # of 32-bit's equivalent of the corresponding rule. # (See `man 7 audit.rules` for details ) [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="" SYSCALL="delete_module" KEY="modules" SYSCALL_GROUPING="delete_module" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20delete_module%20-k%20module-change%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20delete_module%20-k%20module-change%0A mode: 0600 path: /etc/audit/rules.d/75-kernel-module-loading-delete.rules overwrite: true Ensure auditd Collects Information on Kernel Module Loading and Unloading - finit_module If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d to capture kernel module loading and unloading events, setting ARCH to either b32 or b64 as appropriate for your system: -a always,exit -F arch=ARCH -S finit_module -F key=modules If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file in order to capture kernel module loading and unloading events, setting ARCH to either b32 or b64 as appropriate for your system: -a always,exit -F arch=ARCH -S finit_module -F key=modules 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000471-GPOS-00216 SRG-OS-000477-GPOS-00222 SRG-APP-000495-CTR-001235 SRG-APP-000504-CTR-001280 R73 6.3.3.29 The addition/removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system # Note: 32-bit and 64-bit kernel syscall numbers not always line up => # it's required on a 64-bit system to check also for the presence # of 32-bit's equivalent of the corresponding rule. # (See `man 7 audit.rules` for details ) [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="" SYSCALL="finit_module" KEY="modules" SYSCALL_GROUPING="init_module finit_module" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20finit_module%20-k%20module-change%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20finit_module%20-k%20module-change%0A mode: 0600 path: /etc/audit/rules.d/75-kernel-module-loading-finit.rules overwrite: true Ensure auditd Collects Information on Kernel Module Loading - init_module To capture kernel module loading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -S init_module -F key=modules Place to add the line depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the line to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the line to file /etc/audit/audit.rules. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.7 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000471-GPOS-00216 SRG-OS-000477-GPOS-00222 SRG-APP-000495-CTR-001235 SRG-APP-000504-CTR-001280 R73 6.3.3.29 The addition of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system # Note: 32-bit and 64-bit kernel syscall numbers not always line up => # it's required on a 64-bit system to check also for the presence # of 32-bit's equivalent of the corresponding rule. # (See `man 7 audit.rules` for details ) [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="" SYSCALL="init_module" KEY="modules" SYSCALL_GROUPING="init_module finit_module" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20init_module%20-k%20module-change%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20init_module%20-k%20module-change%0A mode: 0600 path: /etc/audit/rules.d/75-kernel-module-loading-init.rules overwrite: true Ensure auditd Collects Information on Kernel Module Loading and Unloading - query_module If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d to capture kernel module loading and unloading events, setting ARCH to either b32 or b64 as appropriate for your system: -a always,exit -F arch=ARCH -S query_module -F auid>=1000 -F auid!=unset -F key=modules If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file in order to capture kernel module loading and unloading events, setting ARCH to either b32 or b64 as appropriate for your system: -a always,exit -F arch=ARCH -S query_module -F auid>=1000 -F auid!=unset -F key=modules 6.3.3.31 The addition/removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ); }; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system # Note: 32-bit and 64-bit kernel syscall numbers not always line up => # it's required on a 64-bit system to check also for the presence # of 32-bit's equivalent of the corresponding rule. # (See `man 7 audit.rules` for details ) [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="query_module" KEY="modules" SYSCALL_GROUPING="init_module query_module" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_kernel_module_loading_query - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set architecture for audit query_module tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - audit_rules_kernel_module_loading_query - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Perform remediation of Audit rules for query_module for x86 platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - query_module syscall_grouping: - init_module - query_module - name: Check existence of query_module in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/module-change.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - query_module syscall_grouping: - init_module - query_module - name: Check existence of query_module in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) tags: - audit_rules_kernel_module_loading_query - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Perform remediation of Audit rules for query_module for x86_64 platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - query_module syscall_grouping: - init_module - query_module - name: Check existence of query_module in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/module-change.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - query_module syscall_grouping: - init_module - query_module - name: Check existence of query_module in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000 -F auid!=unset -F key=module-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - not ( ansible_architecture == "aarch64" ) - audit_arch == "b64" tags: - audit_rules_kernel_module_loading_query - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Record Attempts to Alter Logon and Logout Events The audit system already collects login information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d in order to watch for attempted manual edits of files involved in storing logon events: -w /var/log/tallylog -p wa -k logins -w -p wa -k logins -w /var/log/lastlog -p wa -k logins If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file in order to watch for unattempted manual edits of files involved in storing logon events: -w /var/log/tallylog -p wa -k logins -w -p wa -k logins -w /var/log/lastlog -p wa -k logins Record Attempts to Alter Logon and Logout Events The audit system already collects login information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d in order to watch for attempted manual edits of files involved in storing logon events: -w /var/log/tallylog -p wa -k logins -w -p wa -k logins -w /var/log/lastlog -p wa -k logins If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file in order to watch for unattempted manual edits of files involved in storing logon events: -w /var/log/tallylog -p wa -k logins -w -p wa -k logins -w /var/log/lastlog -p wa -k logins This rule checks for multiple syscalls related to login events; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: audit_rules_login_events_tallylogaudit_rules_login_events_faillockaudit_rules_login_events_lastlog 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.3 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' var_accounts_passwords_pam_faillock_dir='' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/tallylog" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/logins.rules" # If the logins.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/logins.rules" # If the logins.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/lastlog" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/logins.rules" # If the logins.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi Record Attempts to Alter Logon and Logout Events - faillock The audit system already collects login information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w -p wa -k logins If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w -p wa -k logins 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.3 SRG-OS-000392-GPOS-00172 SRG-OS-000470-GPOS-00214 SRG-OS-000473-GPOS-00218 SRG-APP-000503-CTR-001275 SRG-APP-000506-CTR-001290 R73 6.3.3.21 10.2.1.3 10.2.1 10.2 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' var_accounts_passwords_pam_faillock_dir='' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/logins.rules" # If the logins.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_accounts_passwords_pam_faillock_dir # promote to variable set_fact: var_accounts_passwords_pam_faillock_dir: !!str tags: - always - name: Record Attempts to Alter Logon and Logout Events - faillock - Check if watch rule for {{ var_accounts_passwords_pam_faillock_dir }} already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+{{ var_accounts_passwords_pam_faillock_dir }}\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - faillock - Search /etc/audit/rules.d for other rules with specified key logins ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)logins$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - faillock - Use /etc/audit/rules.d/logins.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/logins.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - faillock - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - faillock - Add watch rule for {{ var_accounts_passwords_pam_faillock_dir }} in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w {{ var_accounts_passwords_pam_faillock_dir }} -p wa -k logins create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - faillock - Check if watch rule for {{ var_accounts_passwords_pam_faillock_dir }} already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+{{ var_accounts_passwords_pam_faillock_dir }}\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - faillock - Add watch rule for {{ var_accounts_passwords_pam_faillock_dir }} in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w {{ var_accounts_passwords_pam_faillock_dir }} -p wa -k logins state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_faillock - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Attempts to Alter Logon and Logout Events - lastlog The audit system already collects login information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /var/log/lastlog -p wa -k logins If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /var/log/lastlog -p wa -k logins 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.3 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000473-GPOS-00218 SRG-OS-000470-GPOS-00214 SRG-APP-000495-CTR-001235 SRG-APP-000503-CTR-001275 SRG-APP-000506-CTR-001290 R73 6.3.3.21 10.2.1.3 10.2.1 10.2 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/lastlog" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/logins.rules" # If the logins.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - lastlog - Check if watch rule for /var/log/lastlog already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/log/lastlog\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - lastlog - Search /etc/audit/rules.d for other rules with specified key logins ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)logins$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - lastlog - Use /etc/audit/rules.d/logins.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/logins.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - lastlog - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - lastlog - Add watch rule for /var/log/lastlog in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/log/lastlog -p wa -k logins create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - lastlog - Check if watch rule for /var/log/lastlog already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/log/lastlog\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - lastlog - Add watch rule for /var/log/lastlog in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/log/lastlog -p wa -k logins state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_lastlog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Attempts to Alter Logon and Logout Events - tallylog The audit system already collects login information for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /var/log/tallylog -p wa -k logins If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /var/log/tallylog -p wa -k logins 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.2.3 SRG-OS-000392-GPOS-00172 SRG-OS-000470-GPOS-00214 SRG-OS-000473-GPOS-00218 SRG-APP-000503-CTR-001275 10.2.1.3 10.2.1 10.2 Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/tallylog" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/logins.rules" # If the logins.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - tallylog - Check if watch rule for /var/log/tallylog already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/var/log/tallylog\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - tallylog - Search /etc/audit/rules.d for other rules with specified key logins ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)logins$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - tallylog - Use /etc/audit/rules.d/logins.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/logins.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - tallylog - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - tallylog - Add watch rule for /var/log/tallylog in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /var/log/tallylog -p wa -k logins create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - tallylog - Check if watch rule for /var/log/tallylog already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/var/log/tallylog\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter Logon and Logout Events - tallylog - Add watch rule for /var/log/tallylog in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /var/log/tallylog -p wa -k logins state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.3 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.1 - PCI-DSSv4-10.2.1.3 - audit_rules_login_events_tallylog - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Record Information on the Use of Privileged Commands At a minimum, the audit system should collect the execution of privileged commands for all users and root. Ensure auditd Collects Information on the Use of Privileged Commands - init At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-12(c) SRG-OS-000477-GPOS-00222 Misuse of the init command may cause availability issues for the system. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/init -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_init - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - init - Perform remediation of Audit rules for /usr/sbin/init block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/init -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_init - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - poweroff At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-12(c) SRG-OS-000477-GPOS-00222 Misuse of the poweroff command may cause availability issues for the system. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/poweroff -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_poweroff - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - poweroff - Perform remediation of Audit rules for /usr/sbin/poweroff block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/poweroff -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_poweroff - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - reboot At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-12(c) SRG-OS-000477-GPOS-00222 Misuse of the reboot command may cause availability issues for the system. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/reboot -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_reboot - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - reboot - Perform remediation of Audit rules for /usr/sbin/reboot block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/reboot -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_reboot - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - shutdown At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-12(c) SRG-OS-000477-GPOS-00222 Misuse of the shutdown command may cause availability issues for the system. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/shutdown -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_shutdown - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - shutdown - Perform remediation of Audit rules for /usr/sbin/shutdown block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/shutdown -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(c) - audit_privileged_commands_shutdown - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands The audit system should collect information about usage of privileged commands for all users. These are commands with suid or sgid bits on and they are specially risky in local block device partitions not mounted with noexec and nosuid options. Therefore, these partitions should be first identified by the following command: findmnt -n -l -k -it $(awk '/nodev/ { print $2 }' /proc/filesystems | paste -sd,) | grep -Pv "noexec|nosuid" For all partitions listed by the previous command, it is necessary to search for setuid / setgid programs using the following command: $ sudo find PARTITION -xdev -perm /6000 -type f 2>/dev/null For each setuid / setgid program identified by the previous command, an audit rule must be present in the appropriate place using the following line structure, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit: -a always,exit -F arch=ARCH -F path=PROG_PATH -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup, add the line to a file with suffix .rules in the /etc/audit/rules.d directory, replacing the PROG_PATH part with the full path of that setuid / setgid identified program. If the auditd daemon is configured to use the auditctl utility instead, add the line to the /etc/audit/audit.rules file, also replacing the PROG_PATH part with the full path of that setuid / setgid identified program. This rule checks for multiple syscalls related to privileged commands. If needed to check specific privileged commands, other more specific rules should be considered. For example: audit_rules_privileged_commands_suaudit_rules_privileged_commands_umountaudit_rules_privileged_commands_passwd Note that OVAL check and Bash / Ansible remediation of this rule explicitly excludes file systems mounted at /proc directory and its subdirectories. It is a virtual file system and it doesn't contain executable applications. At the same time, interacting with this file system during check or remediation caused undesirable errors. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO08.04 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.05 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.5 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.3.4.5.9 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 3.9 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 0582 0584 05885 0586 0846 0957 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.1 A.16.1.2 A.16.1.3 A.16.1.4 A.16.1.5 A.16.1.7 A.6.1.3 A.6.2.1 A.6.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-2 DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 DE.DP-4 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 RS.CO-2 Req-10.2.2 SRG-OS-000327-GPOS-00127 R73 6.3.3.10 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern that can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then ACTION_ARCH_FILTERS="-a always,exit" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" function add_audit_rule() { local PRIV_CMD="$1" local OTHER_FILTERS="-F path=$PRIV_CMD -F perm=x" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi } if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then PRIV_CMDS=$(find / -perm /6000 -type f -not -path "/sysroot/*" 2>/dev/null) for PRIV_CMD in $PRIV_CMDS; do add_audit_rule $PRIV_CMD done else FILTER_NODEV=$(awk '/nodev/ { print $2 }' /proc/filesystems | paste -sd,) PARTITIONS=$(findmnt -n -l -k -it "$FILTER_NODEV" | grep -Pv "noexec|nosuid|/proc($|/.*$)" | awk '{ print $1 }') for PARTITION in $PARTITIONS; do PRIV_CMDS=$(find "${PARTITION}" -xdev -perm /6000 -type f 2>/dev/null) for PRIV_CMD in $PRIV_CMDS; do add_audit_rule $PRIV_CMD done done fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - audit_rules_privileged_commands - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure auditd Collects Information on the Use of Privileged Commands - Set List of Mount Points Which Permits Execution of Privileged Commands ansible.builtin.set_fact: privileged_mount_points: '{{ (ansible_facts.mounts | rejectattr(''options'', ''search'', ''noexec|nosuid'') | rejectattr(''mount'', ''match'', ''/proc($|/.*$)'') | map(attribute=''mount'') | list ) }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - audit_rules_privileged_commands - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure auditd Collects Information on the Use of Privileged Commands - Search for Privileged Commands in Eligible Mount Points ansible.builtin.shell: cmd: find {{ item }} -xdev -perm /6000 -type f 2>/dev/null register: result_privileged_commands_search changed_when: false failed_when: false with_items: '{{ privileged_mount_points }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - audit_rules_privileged_commands - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure auditd Collects Information on the Use of Privileged Commands - Set List of Privileged Commands Found in Eligible Mount Points ansible.builtin.set_fact: privileged_commands: '{{ privileged_commands | default([]) + item.stdout_lines }}' loop: '{{ result_privileged_commands_search.results }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - item is not skipped tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - audit_rules_privileged_commands - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure auditd Collects Information on the Use of Privileged Commands - Privileged Commands are Present in the System block: - name: Ensure auditd Collects Information on the Use of Privileged Commands - Ensure Rules for All Privileged Commands in augenrules Format ansible.builtin.lineinfile: path: /etc/audit/rules.d/privileged.rules line: -a always,exit -F path={{ item }} -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged regexp: ^.*path={{ item | regex_escape() }} .*$ create: true with_items: - '{{ privileged_commands }}' - name: Ensure auditd Collects Information on the Use of Privileged Commands - Ensure Rules for All Privileged Commands in auditctl Format ansible.builtin.lineinfile: path: /etc/audit/audit.rules line: -a always,exit -F path={{ item }} -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged regexp: ^.*path={{ item | regex_escape() }} .*$ create: true with_items: - '{{ privileged_commands }}' - name: Ensure auditd Collects Information on the Use of Privileged Commands - Search for Duplicated Rules in Other Files ansible.builtin.find: paths: /etc/audit/rules.d recurse: false contains: ^-a always,exit -F path={{ item }} .*$ patterns: '*.rules' with_items: - '{{ privileged_commands }}' register: result_augenrules_files - name: Ensure auditd Collects Information on the Use of Privileged Commands - Ensure Rules for Privileged Commands are Defined Only in One File ansible.builtin.lineinfile: path: '{{ item.1.path }}' regexp: ^-a always,exit -F path={{ item.0.item }} .*$ state: absent with_subelements: - '{{ result_augenrules_files.results }}' - files when: - item.1.path != '/etc/audit/rules.d/privileged.rules' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - privileged_commands is defined tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.2.2 - audit_rules_privileged_commands - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Ensure auditd Collects Information on the Use of Privileged Commands - at At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-2(d) AU-12(c) AC-6(9) CM-6(a) Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/at -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_at - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - at - Perform remediation of Audit rules for /usr/bin/at block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/at -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_at - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - chage At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000468-GPOS-00212 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 SRG-APP-000501-CTR-001265 SRG-APP-000502-CTR-001270 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/chage -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_chage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - chage - Perform remediation of Audit rules for /usr/bin/chage block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_chage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - chsh At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/chsh -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_chsh - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - chsh - Perform remediation of Audit rules for /usr/bin/chsh block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_chsh - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - crontab At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/crontab -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - crontab - Perform remediation of Audit rules for /usr/bin/crontab block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_crontab - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - gpasswd At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/gpasswd -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_gpasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - gpasswd - Perform remediation of Audit rules for /usr/bin/gpasswd block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_gpasswd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - insmod At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -w /sbin/insmod -p x -k modules AU-12(c) AU-12.1(iv) AU-3 AU-3.1 AU-12(a) AU-12.1(ii) MA-4(1)(a) SRG-OS-000037-GPOS-00015 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 R73 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. Ensure auditd Collects Information on the Use of Privileged Commands - kmod At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-3 AU-3.1 AU-12(a) AU-12.1(ii) AU-12.1(iv)AU-12(c) MA-4(1)(a) SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000471-GPOS-00216 SRG-OS-000477-GPOS-00222 SRG-APP-000495-CTR-001235 SRG-APP-000504-CTR-001280 R73 6.3.3.28 Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter). # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/kmod -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(a) - NIST-800-53-AU-12.1(ii) - NIST-800-53-AU-12.1(iv)AU-12(c) - NIST-800-53-AU-3 - NIST-800-53-AU-3.1 - NIST-800-53-MA-4(1)(a) - audit_rules_privileged_commands_kmod - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - kmod - Perform remediation of Audit rules for /usr/bin/kmod block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(a) - NIST-800-53-AU-12.1(ii) - NIST-800-53-AU-12.1(iv)AU-12(c) - NIST-800-53-AU-3 - NIST-800-53-AU-3.1 - NIST-800-53-MA-4(1)(a) - audit_rules_privileged_commands_kmod - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - modprobe At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -w /sbin/modprobe -p x -k modules If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -w /sbin/modprobe -p x -k modules AU-12(a) AU-12.1(ii) AU-3 AU-3.1 AU-12(c) AU-12.1(iv) MA-4(1)(a) SRG-OS-000037-GPOS-00015 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 R73 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. Ensure auditd Collects Information on the Use of Privileged Commands - mount At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged AU-2(d) AU-12(c) AC-6(9) CM-6(a) SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/mount -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_mount - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - mount - Perform remediation of Audit rules for /usr/bin/mount block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_mount - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - newgidmap At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/newgidmap -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_newgidmap - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - newgidmap - Perform remediation of Audit rules for /usr/bin/newgidmap block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_newgidmap - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - newgrp At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/newgrp -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_newgrp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - newgrp - Perform remediation of Audit rules for /usr/bin/newgrp block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_newgrp - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - newuidmap At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/newuidmap -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_newuidmap - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - newuidmap - Perform remediation of Audit rules for /usr/bin/newuidmap block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newuidmap -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_newuidmap - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - pam_timestamp_check At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/pam_timestamp_check -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_pam_timestamp_check - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - pam_timestamp_check - Perform remediation of Audit rules for /usr/sbin/pam_timestamp_check block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_pam_timestamp_check - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - passwd At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/passwd -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - passwd - Perform remediation of Audit rules for /usr/bin/passwd block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_passwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - postdrop At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/postdrop -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_postdrop - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - postdrop - Perform remediation of Audit rules for /usr/sbin/postdrop block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_postdrop - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - postqueue At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/postqueue -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_postqueue - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - postqueue - Perform remediation of Audit rules for /usr/sbin/postqueue block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_postqueue - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - rmmod At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -w /sbin/rmmod -p x -k modules SRG-OS-000037-GPOS-00015 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 R73 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. Record Any Attempts to Run ssh-agent At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter). # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/ssh-agent -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_privileged_commands_ssh_agent - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Any Attempts to Run ssh-agent - Perform remediation of Audit rules for /usr/bin/ssh-agent block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_privileged_commands_ssh_agent - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - ssh-keysign At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/libexec/openssh/ssh-keysign -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_ssh_keysign - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - ssh-keysign - Perform remediation of Audit rules for /usr/libexec/openssh/ssh-keysign block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_ssh_keysign - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - su At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000064-GPOS-00033 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-OS-000755-GPOS-00220 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/su -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_su - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - su - Perform remediation of Audit rules for /usr/bin/su block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_su - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - sudo At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 SRG-OS-000755-GPOS-00220 R33 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/sudo -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_sudo - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - sudo - Perform remediation of Audit rules for /usr/bin/sudo block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_sudo - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - sudoedit At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 SRG-OS-000755-GPOS-00220 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/sudoedit -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_sudoedit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - sudoedit - Perform remediation of Audit rules for /usr/bin/sudoedit block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_sudoedit - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - umount At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/bin/umount -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_umount - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - umount - Perform remediation of Audit rules for /usr/bin/umount block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_umount - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - unix_chkpwd At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 CIP-007-3 R6.5 AC-2(4) AU-2(d) AU-3 AU-3.1 AU-12(a) AU-12(c) AU-12.1(ii) AU-12.1(iv) AC-6(9) CM-6(a) MA-4(1)(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000029-CTR-000085 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/unix_chkpwd -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(a) - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(ii) - NIST-800-53-AU-12.1(iv) - NIST-800-53-AU-2(d) - NIST-800-53-AU-3 - NIST-800-53-AU-3.1 - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(1)(a) - audit_rules_privileged_commands_unix_chkpwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - unix_chkpwd - Perform remediation of Audit rules for /usr/sbin/unix_chkpwd block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(a) - NIST-800-53-AU-12(c) - NIST-800-53-AU-12.1(ii) - NIST-800-53-AU-12.1(iv) - NIST-800-53-AU-2(d) - NIST-800-53-AU-3 - NIST-800-53-AU-3.1 - NIST-800-53-CM-6(a) - NIST-800-53-MA-4(1)(a) - audit_rules_privileged_commands_unix_chkpwd - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - userhelper At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-APP-000495-CTR-001235 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/userhelper -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_userhelper - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - userhelper - Perform remediation of Audit rules for /usr/sbin/userhelper block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_userhelper - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - usermod At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged SRG-OS-000037-GPOS-00015 SRG-OS-000042-GPOS-00020 SRG-OS-000062-GPOS-00031 SRG-OS-000392-GPOS-00172 SRG-OS-000462-GPOS-00206 SRG-OS-000471-GPOS-00215 SRG-OS-000466-GPOS-00210 SRG-APP-000495-CTR-001235 SRG-APP-000499-CTR-001255 6.3.3.27 Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/usermod -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - audit_rules_privileged_commands_usermod - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - usermod - Perform remediation of Audit rules for /usr/sbin/usermod block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - audit_rules_privileged_commands_usermod - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Ensure auditd Collects Information on the Use of Privileged Commands - usernetctl At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules: -a always,exit -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged CIP-004-6 R2.2.2 CIP-004-6 R2.2.3 CIP-007-3 R.1.3 CIP-007-3 R5 CIP-007-3 R5.1.1 CIP-007-3 R5.1.3 CIP-007-3 R5.2.1 CIP-007-3 R5.2.3 AC-2(4) AU-2(d) AU-12(c) AC-6(9) CM-6(a) Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system OTHER_FILTERS="-F path=/usr/sbin/usernetctl -F perm=x" AUID_FILTERS="-F auid>=1000 -F auid!=unset" SYSCALL="" KEY="privileged" SYSCALL_GROUPING="" ACTION_ARCH_FILTERS="-a always,exit" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_usernetctl - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Ensure auditd Collects Information on the Use of Privileged Commands - usernetctl - Perform remediation of Audit rules for /usr/sbin/usernetctl block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/privileged.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: [] syscall_grouping: [] - name: Check existence of in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/usernetctl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AC-2(4) - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - audit_rules_privileged_commands_usernetctl - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Records Events that Modify Date and Time Information Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time. All changes to the system time should be audited. Record attempts to alter time through adjtimex If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S adjtimex -F key=audit_time_rules If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S adjtimex -F key=audit_time_rules If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S adjtimex -F key=audit_time_rules If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S adjtimex -F key=audit_time_rules The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls: -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.4.2.b R73 6.3.3.4 10.6.3 10.6 Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do # Create expected audit group and audit rule form for particular system call & architecture if [ ${ARCH} = "b32" ] then ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" # stime system call is known at 32-bit arch (see e.g "$ ausyscall i386 stime" 's output) # so append it to the list of time group system calls to be audited SYSCALL="adjtimex settimeofday stime" SYSCALL_GROUPING="adjtimex settimeofday stime" elif [ ${ARCH} = "b64" ] then ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" # stime system call isn't known at 64-bit arch (see "$ ausyscall x86_64 stime" 's output) # therefore don't add it to the list of time group system calls to be audited SYSCALL="adjtimex settimeofday" SYSCALL_GROUPING="adjtimex settimeofday" fi OTHER_FILTERS="" AUID_FILTERS="" KEY="audit_time_rules" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_adjtimex - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set architecture for audit tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_adjtimex - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Perform remediation of Audit rules for adjtimex for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - adjtimex syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of adjtimex in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/audit_time_rules.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - adjtimex syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of adjtimex in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_adjtimex - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Perform remediation of Audit rules for adjtimex for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - adjtimex syscall_grouping: - adjtimex - settimeofday - name: Check existence of adjtimex in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/audit_time_rules.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - adjtimex syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of adjtimex in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_adjtimex - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20adjtimex%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20adjtimex%20-k%20audit_time_rules%0A }} mode: 0600 path: /etc/audit/rules.d/75-syscall-adjtimex.rules overwrite: true Record Attempts to Alter Time Through clock_settime If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S clock_settime -F a0=0x0 -F key=time-change If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S clock_settime -F a0=0x0 -F key=time-change If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S clock_settime -F a0=0x0 -F key=time-change If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S clock_settime -F a0=0x0 -F key=time-change The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls: -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.4.2.b R73 6.3.3.4 10.6.3 10.6 Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # First perform the remediation of the syscall rule # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" OTHER_FILTERS="-F a0=0x0" AUID_FILTERS="" SYSCALL="clock_settime" KEY="time-change" SYSCALL_GROUPING="" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_clock_settime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set architecture for audit tasks ansible.builtin.set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_clock_settime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Perform remediation of Audit rules for clock_settime for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - clock_settime syscall_grouping: [] - name: Check existence of clock_settime in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a0=0x0 (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/time-change.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/time-change.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a0=0x0 -F key=time-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - clock_settime syscall_grouping: [] - name: Check existence of clock_settime in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a0=0x0 (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a0=0x0 -F key=time-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_clock_settime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Perform remediation of Audit rules for clock_settime for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - clock_settime syscall_grouping: [] - name: Check existence of clock_settime in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a0=0x0 (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/time-change.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/time-change.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a0=0x0 -F key=time-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - clock_settime syscall_grouping: [] - name: Check existence of clock_settime in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F a0=0x0 (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a0=0x0 -F key=time-change create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_clock_settime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20clock_settime%20-F%20a0%3D0x0%20-k%20time-change%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20clock_settime%20-F%20a0%3D0x0%20-k%20time-change%0A }} mode: 0600 path: /etc/audit/rules.d/75-syscall-clock-settime.rules overwrite: true Record attempts to alter time through settimeofday If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d: -a always,exit -F arch=b32 -S settimeofday -F key=audit_time_rules If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S settimeofday -F key=audit_time_rules If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file: -a always,exit -F arch=b32 -S settimeofday -F key=audit_time_rules If the system is 64 bit then also add the following line: -a always,exit -F arch=b64 -S settimeofday -F key=audit_time_rules The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls: -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.4.2.b 6.3.3.4 10.6.3 10.6 Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do # Create expected audit group and audit rule form for particular system call & architecture if [ ${ARCH} = "b32" ] then ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" # stime system call is known at 32-bit arch (see e.g "$ ausyscall i386 stime" 's output) # so append it to the list of time group system calls to be audited SYSCALL="adjtimex settimeofday stime" SYSCALL_GROUPING="adjtimex settimeofday stime" elif [ ${ARCH} = "b64" ] then ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" # stime system call isn't known at 64-bit arch (see "$ ausyscall x86_64 stime" 's output) # therefore don't add it to the list of time group system calls to be audited SYSCALL="adjtimex settimeofday" SYSCALL_GROUPING="adjtimex settimeofday" fi OTHER_FILTERS="" AUID_FILTERS="" KEY="audit_time_rules" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_settimeofday - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set architecture for audit tasks set_fact: audit_arch: b64 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_settimeofday - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Perform remediation of Audit rules for settimeofday for 32bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - settimeofday syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of settimeofday in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/audit_time_rules.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - settimeofday syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of settimeofday in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_settimeofday - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Perform remediation of Audit rules for settimeofday for 64bit platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - settimeofday syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of settimeofday in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/audit_time_rules.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - settimeofday syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of settimeofday in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - audit_arch == "b64" tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_settimeofday - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20settimeofday%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20settimeofday%20-k%20audit_time_rules%0A }} mode: 0600 path: /etc/audit/rules.d/75-syscall-settimeofday.rules overwrite: true Record Attempts to Alter Time Through stime If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d for both 32 bit and 64 bit systems: -a always,exit -F arch=b32 -S stime -F key=audit_time_rules Since the 64 bit version of the "stime" system call is not defined in the audit lookup table, the corresponding "-F arch=b64" form of this rule is not expected to be defined on 64 bit systems (the aforementioned "-F arch=b32" stime rule form itself is sufficient for both 32 bit and 64 bit systems). If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file for both 32 bit and 64 bit systems: -a always,exit -F arch=b32 -S stime -F key=audit_time_rules Since the 64 bit version of the "stime" system call is not defined in the audit lookup table, the corresponding "-F arch=b64" form of this rule is not expected to be defined on 64 bit systems (the aforementioned "-F arch=b32" stime rule form itself is sufficient for both 32 bit and 64 bit systems). The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined system calls: -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.4.2.b R73 10.6.3 10.6 Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel && { ( ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ) && ! ( ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) ) ); }; then # Retrieve hardware architecture of the underlying system [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64") for ARCH in "${RULE_ARCHS[@]}" do # Create expected audit group and audit rule form for particular system call & architecture if [ ${ARCH} = "b32" ] then ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" # stime system call is known at 32-bit arch (see e.g "$ ausyscall i386 stime" 's output) # so append it to the list of time group system calls to be audited SYSCALL="adjtimex settimeofday stime" SYSCALL_GROUPING="adjtimex settimeofday stime" elif [ ${ARCH} = "b64" ] then ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH" # stime system call isn't known at 64-bit arch (see "$ ausyscall x86_64 stime" 's output) # therefore don't add it to the list of time group system calls to be audited SYSCALL="adjtimex settimeofday" SYSCALL_GROUPING="adjtimex settimeofday" fi OTHER_FILTERS="" AUID_FILTERS="" KEY="audit_time_rules" # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'augenrules', then check if the audit rule is defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection default_file="/etc/audit/rules.d/$KEY.rules" # As other_filters may include paths, lets use a different delimiter for it # The "F" script expression tells sed to print the filenames where the expressions matched readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules) # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet if [ ${#files_to_inspect[@]} -eq "0" ] then file_to_inspect="/etc/audit/rules.d/$KEY.rules" files_to_inspect=("$file_to_inspect") if [ ! -e "$file_to_inspect" ] then touch "$file_to_inspect" chmod 0600 "$file_to_inspect" fi fi # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi unset syscall_a unset syscall_grouping unset syscall_string unset syscall unset file_to_edit unset rule_to_edit unset rule_syscalls_to_edit unset other_string unset auid_string unset full_rule # Load macro arguments into arrays read -a syscall_a <<< $SYSCALL read -a syscall_grouping <<< $SYSCALL_GROUPING # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- # files_to_inspect=() # If audit tool is 'auditctl', then add '/etc/audit/audit.rules' # file to the list of files to be inspected default_file="/etc/audit/audit.rules" files_to_inspect+=('/etc/audit/audit.rules' ) # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead skip=1 for audit_file in "${files_to_inspect[@]}" do # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern, # i.e, collect rules that match: # * the action, list and arch, (2-nd argument) # * the other filters, (3-rd argument) # * the auid filters, (4-rd argument) readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file") candidate_rules=() # Filter out rules that have more fields then required. This will remove rules more specific than the required scope for s_rule in "${similar_rules[@]}" do # Strip all the options and fields we know of, # than check if there was any field left over extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//" -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule") grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule") done if [[ ${#syscall_a[@]} -ge 1 ]] then # Check if the syscall we want is present in any of the similar existing rules for rule in "${candidate_rules[@]}" do rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs) all_syscalls_found=0 for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || { # A syscall was not found in the candidate rule all_syscalls_found=1 } done if [[ $all_syscalls_found -eq 0 ]] then # We found a rule with all the syscall(s) we want; skip rest of macro skip=0 break fi # Check if this rule can be grouped with our target syscall and keep track of it for syscall_g in "${syscall_grouping[@]}" do if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls" then file_to_edit=${audit_file} rule_to_edit=${rule} rule_syscalls_to_edit=${rule_syscalls} fi done done else # If there is any candidate rule, it is compliant; skip rest of macro if [ "${#candidate_rules[@]}" -gt 0 ] then skip=0 fi fi if [ "$skip" -eq 0 ]; then break fi done if [ "$skip" -ne 0 ]; then # We checked all rules that matched the expected resemblance pattern (action, arch & auid) # At this point we know if we need to either append the $full_rule or group # the syscall together with an exsiting rule # Append the full_rule if it cannot be grouped to any other rule if [ -z ${rule_to_edit+x} ] then # Build full_rule while avoid adding double spaces when other_filters is empty if [ "${#syscall_a[@]}" -gt 0 ] then syscall_string="" for syscall in "${syscall_a[@]}" do syscall_string+=" -S $syscall" done fi other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true echo "$full_rule" >> "$default_file" chmod 0600 ${default_file} else # Check if the syscalls are declared as a comma separated list or # as multiple -S parameters if grep -q -- "," <<< "${rule_syscalls_to_edit}" then delimiter="," else delimiter=" -S " fi new_grouped_syscalls="${rule_syscalls_to_edit}" for syscall in "${syscall_a[@]}" do grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || { # A syscall was not found in the candidate rule new_grouped_syscalls+="${delimiter}${syscall}" } done # Group the syscall in the rule sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit" fi fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_stime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Perform remediation of Audit rules for stime syscall for x86 platform block: - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - stime syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of stime in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: '*.rules' register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Reset syscalls found per file ansible.builtin.set_fact: syscalls_per_file: {} found_paths_dict: {} - name: Declare syscalls found per file ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}" loop: '{{ find_command.results | selectattr(''matched'') | list }}' - name: Declare files where syscalls were found ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten | map(attribute='path') | list }}" - name: Count occurrences of syscalls in paths ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item, 0) }) }}" loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'') | list }}' - name: Get path with most syscalls ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value') | last).key }}" when: found_paths | length >= 1 - name: No file with syscall found, set path to /etc/audit/rules.d/audit_time_rules.rules ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules" when: found_paths | length == 0 - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file] | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 - name: Declare list of syscalls ansible.builtin.set_fact: syscalls: - stime syscall_grouping: - adjtimex - settimeofday - stime - name: Check existence of stime in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$ patterns: audit.rules register: find_command loop: '{{ (syscall_grouping + syscalls) | unique }}' - name: Set path to /etc/audit/audit.rules ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules" - name: Declare found syscalls ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item') | list }}" - name: Declare missing syscalls ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}" - name: Replace the audit rule in {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+) line: \1\2\3{{ missing_syscalls | join("\3") }}\4 backrefs: true state: present mode: g-rwx,o-rwx when: syscalls_found | length > 0 and missing_syscalls | length > 0 - name: Add the audit rule to {{ audit_file }} ansible.builtin.lineinfile: path: '{{ audit_file }}' line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules create: true mode: g-rwx,o-rwx state: present when: syscalls_found | length == 0 when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - ( not ( ansible_architecture == "aarch64" ) and not ( ansible_architecture == "s390x" ) ) tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_stime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20stime%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20stime%20-k%20audit_time_rules%0A }} mode: 0600 path: /etc/audit/rules.d/75-syscall-stime.rules overwrite: true Record Attempts to Alter the localtime File If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d: -w /etc/localtime -p wa -k audit_time_rules If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules: -w /etc/localtime -p wa -k audit_time_rules 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 9 5.4.1.1 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 APO12.06 APO13.01 BAI03.05 BAI08.02 DSS01.03 DSS01.04 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS03.05 DSS05.02 DSS05.03 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.1.7 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.2.3.10 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.3.6.6 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 1.13 SR 2.10 SR 2.11 SR 2.12 SR 2.6 SR 2.8 SR 2.9 SR 3.1 SR 3.5 SR 3.8 SR 4.1 SR 4.3 SR 5.1 SR 5.2 SR 5.3 SR 6.1 SR 6.2 SR 7.1 SR 7.6 A.11.2.6 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.13.1.1 A.13.2.1 A.14.1.3 A.14.2.7 A.15.2.1 A.15.2.2 A.16.1.4 A.16.1.5 A.16.1.7 A.6.2.1 A.6.2.2 AU-2(d) AU-12(c) AC-6(9) CM-6(a) DE.AE-3 DE.AE-5 DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.AC-3 PR.PT-1 PR.PT-4 RS.AN-1 RS.AN-4 Req-10.4.2.b R73 6.3.3.4 10.6.3 10.6 Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then # Perform the remediation for both possible tools: 'auditctl' and 'augenrules' # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules' # into the list of files to be inspected files_to_inspect+=('/etc/audit/audit.rules') # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/localtime" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/localtime $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/localtime$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/localtime -p wa -k audit_time_rules" >> "$audit_rules_file" fi done # Create a list of audit *.rules files that should be inspected for presence and correctness # of a particular audit rule. The scheme is as follows: # # ----------------------------------------------------------------------------------------- # Tool used to load audit rules | Rule already defined | Audit rules file to inspect | # ----------------------------------------------------------------------------------------- # auditctl | Doesn't matter | /etc/audit/audit.rules | # ----------------------------------------------------------------------------------------- # augenrules | Yes | /etc/audit/rules.d/*.rules | # augenrules | No | /etc/audit/rules.d/$key.rules | # ----------------------------------------------------------------------------------------- files_to_inspect=() # If the audit is 'augenrules', then check if rule is already defined # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection. # If rule isn't defined, add '/etc/audit/rules.d/audit_time_rules.rules' to list of files for inspection. readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/localtime" /etc/audit/rules.d/*.rules) # For each of the matched entries for match in "${matches[@]}" do # Extract filepath from the match rulesd_audit_file=$(echo $match | cut -f1 -d ':') # Append that path into list of files for inspection files_to_inspect+=("$rulesd_audit_file") done # Case when particular audit rule isn't defined yet if [ "${#files_to_inspect[@]}" -eq "0" ] then # Append '/etc/audit/rules.d/audit_time_rules.rules' into list of files for inspection key_rule_file="/etc/audit/rules.d/audit_time_rules.rules" # If the audit_time_rules.rules file doesn't exist yet, create it with correct permissions if [ ! -e "$key_rule_file" ] then touch "$key_rule_file" chmod 0600 "$key_rule_file" fi files_to_inspect+=("$key_rule_file") fi # Finally perform the inspection and possible subsequent audit rule # correction for each of the files previously identified for inspection for audit_rules_file in "${files_to_inspect[@]}" do # Check if audit watch file system object rule for given path already present if grep -q -P -- "^[\s]*-w[\s]+/etc/localtime" "$audit_rules_file" then # Rule is found => verify yet if existing rule definition contains # all of the required access type bits # Define BRE whitespace class shortcut sp="[[:space:]]" # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/localtime $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file") # Split required access bits string into characters array # (to check bit's presence for one bit at a time) for access_bit in $(echo "wa" | grep -o .) do # For each from the required access bits (e.g. 'w', 'a') check # if they are already present in current access bits for rule. # If not, append that bit at the end if ! grep -q "$access_bit" <<< "$current_access_bits" then # Concatenate the existing mask with the missing bit current_access_bits="$current_access_bits$access_bit" fi done # Propagate the updated rule's access bits (original + the required # ones) back into the /etc/audit/audit.rules file for that rule sed -i "s#\($sp*-w$sp\+/etc/localtime$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file" else # Rule isn't present yet. Append it at the end of $audit_rules_file file # with proper key echo "-w /etc/localtime -p wa -k audit_time_rules" >> "$audit_rules_file" fi done else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter the localtime File - Check if watch rule for /etc/localtime already exists in /etc/audit/rules.d/ ansible.builtin.find: paths: /etc/audit/rules.d contains: ^\s*-w\s+/etc/localtime\s+-p\s+wa(\s|$)+ patterns: '*.rules' register: find_existing_watch_rules_d when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter the localtime File - Search /etc/audit/rules.d for other rules with specified key audit_time_rules ansible.builtin.find: paths: /etc/audit/rules.d contains: ^.*(?:-F key=|-k\s+)audit_time_rules$ patterns: '*.rules' register: find_watch_key when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter the localtime File - Use /etc/audit/rules.d/audit_time_rules.rules as the recipient for the rule ansible.builtin.set_fact: all_files: - /etc/audit/rules.d/audit_time_rules.rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter the localtime File - Use matched file as the recipient for the rule ansible.builtin.set_fact: all_files: - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter the localtime File - Add watch rule for /etc/localtime in /etc/audit/rules.d/ ansible.builtin.lineinfile: path: '{{ all_files[0] }}' line: -w /etc/localtime -p wa -k audit_time_rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter the localtime File - Check if watch rule for /etc/localtime already exists in /etc/audit/audit.rules ansible.builtin.find: paths: /etc/audit/ contains: ^\s*-w\s+/etc/localtime\s+-p\s+wa(\s|$)+ patterns: audit.rules register: find_existing_watch_audit_rules when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Record Attempts to Alter the localtime File - Add watch rule for /etc/localtime in /etc/audit/audit.rules ansible.builtin.lineinfile: line: -w /etc/localtime -p wa -k audit_time_rules state: present dest: /etc/audit/audit.rules create: true mode: '0600' when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched == 0 tags: - CJIS-5.4.1.1 - NIST-800-171-3.1.7 - NIST-800-53-AC-6(9) - NIST-800-53-AU-12(c) - NIST-800-53-AU-2(d) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.4.2.b - PCI-DSSv4-10.6 - PCI-DSSv4-10.6.3 - audit_rules_time_watch_localtime - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Configure auditd Data Retention The audit system writes data to /var/log/audit/audit.log. By default, auditd rotates 5 logs by size (6MB), retaining a maximum of 30MB of data in total, and refuses to write entries when the disk is too full. This minimizes the risk of audit data filling its partition and impacting other services. This also minimizes the risk of the audit daemon temporarily disabling the system if it cannot write audit log (which it can be configured to do). For a busy system or a system which is thoroughly auditing system activity, the default settings for data retention may be insufficient. The log file size needed will depend heavily on what types of events are being audited. First configure auditing to log all the events of interest. Then monitor the log size manually for awhile to determine what file size will allow you to keep the required data for the correct time period. Using a dedicated partition for /var/log/audit prevents the auditd logs from disrupting system functionality if they fill, and, more importantly, prevents other activity in /var from filling the partition and stopping the audit trail. (The audit logs are size-limited and therefore unlikely to grow without bound unless configured to do so.) Some machines may have requirements that no actions occur which cannot be audited. If this is the case, then auditd can be configured to halt the machine if it runs out of space. Note: Since older logs are rotated, configuring auditd this way does not prevent older logs from being rotated away before they can be viewed. If your system is configured to halt when logging cannot be performed, make sure this can never happen under normal circumstances! Ensure that /var/log/audit is on its own partition, and that this partition is larger than the maximum amount of data auditd will retain normally. Remote server for audispd to send audit records The configuration file could be "/etc/audit/audisp-remote.conf" or "/etc/audisp/audisp-remote.conf" depending on the distro logcollector Account for auditd to send email when actions occurs The setting for action_mail_acct in /etc/audit/auditd.conf admin root root Action for auditd to take when disk space is low The setting for admin_space_left_action in /etc/audit/auditd.conf single email exec halt single suspend syslog rotate ignore single|halt single|halt The percentage remaining in disk space before prompting admin_space_left_action The setting for admin_space_left as a percentage in /etc/audit/auditd.conf 5 25 50 75 5 Action for auditd to take when disk errors 'The setting for disk_error_action in /etc/audit/auditd.conf, if multiple values are allowed write them separated by pipes as in "syslog|single|halt", for remediations the first value will be taken' single exec halt single suspend syslog ignore syslog|single|halt syslog|single|halt syslog|single|halt syslog|single|halt syslog|single|halt syslog|single|halt Action for auditd to take when disk is full 'The setting for disk_full_action in /etc/audit/auditd.conf, if multiple values are allowed write them separated by pipes as in "syslog|single|halt", for remediations the first value will be taken' single exec halt single suspend syslog ignore rotate syslog|single|halt syslog|single|halt syslog|single|halt halt|single halt|single halt|single Auditd priority for flushing data to disk The setting for flush in /etc/audit/auditd.conf data data incremental incremental_async none sync Number of Record to Retain Before Flushing to Disk The setting for freq in /etc/audit/auditd.conf 50 100 50 Maximum audit log file size for auditd The setting for max_log_file in /etc/audit/auditd.conf 1 10 20 5 6 6 Action for auditd to take when log files reach their maximum size The setting for max_log_file_action in /etc/audit/auditd.conf. The following options are available: ignore - audit daemon does nothing. syslog - audit daemon will issue a warning to syslog. suspend - audit daemon will stop writing records to the disk. rotate - audit daemon will rotate logs in the same convention used by logrotate. keep_logs - similar to rotate but prevents audit logs to be overwritten. May trigger space_left_action if volume is full. rotate keep_logs rotate suspend syslog ignore Type of hostname to record the audit event Type of hostname to record the audit event hostname hostname fqd numeric user none hostname|fqd|numeric Number of log files for auditd to retain The setting for num_logs in /etc/audit/auditd.conf 0 1 2 3 4 5 10 20 50 100 5 Size remaining in disk space before prompting space_left_action The setting for space_left (MB) in /etc/audit/auditd.conf 1000 100 250 500 750 100 Action for auditd to take when disk space just starts to run low The setting for space_left_action in /etc/audit/auditd.conf email email exec halt single suspend syslog rotate ignore email|exec|single|halt email|exec|single|halt The percentage remaining in disk space before prompting space_left_action The setting for space_left as a percentage in /etc/audit/auditd.conf 25 50 75 25 Configure audispd Plugin To Send Logs To Remote Server Configure the audispd plugin to off-load audit records onto a different system or media from the system being audited. Set the remote_server option in /etc/audit/audisp-remote.conf with an IP address or hostname of the system that the audispd plugin should send audit records to. For example remote_server = SRG-OS-000342-GPOS-00133 SRG-OS-000479-GPOS-00224 Information stored in one location is vulnerable to accidental or incidental deletion or alteration.Off-loading is a common process in information systems with limited audit storage capacity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_audispd_remote_server='' AUDITCONFIG=/etc/audit/audisp-remote.conf if [ -e "$AUDITCONFIG" ] ; then LC_ALL=C sed -i "/^\s*remote_server\s*=\s*/Id" "$AUDITCONFIG" else printf '%s\n' "Path '$AUDITCONFIG' wasn't found on this system. Refusing to continue." >&2 return 1 fi # make sure file has newline at the end sed -i -e '$a\' "$AUDITCONFIG" cp "$AUDITCONFIG" "$AUDITCONFIG.bak" # Insert at the end of the file printf '%s\n' "remote_server = $var_audispd_remote_server" >> "$AUDITCONFIG" # Clean up after ourselves. rm "$AUDITCONFIG.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - auditd_audispd_configure_remote_server - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: XCCDF Value var_audispd_remote_server # promote to variable set_fact: var_audispd_remote_server: !!str tags: - always - name: Configure audispd Plugin To Send Logs To Remote Server - Make sure that a remote server is configured for Audispd ansible.builtin.lineinfile: path: /etc/audit/audisp-remote.conf line: remote_server = {{ var_audispd_remote_server }} regexp: ^\s*remote_server\s*=.*$ create: true state: present when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - auditd_audispd_configure_remote_server - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Encrypt Audit Records Sent With audispd Plugin Configure the operating system to encrypt the transfer of off-loaded audit records onto a different system or media from the system being audited. Set the transport option in /etc/audit/audisp-remote.conf to KRB5. AU-9(3) CM-6(a) SRG-OS-000342-GPOS-00133 SRG-OS-000479-GPOS-00224 Information stored in one location is vulnerable to accidental or incidental deletion or alteration. Off-loading is a common process in information systems with limited audit storage capacity. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then AUDISP_REMOTE_CONFIG="/etc/audit/audisp-remote.conf" option="^transport" value="KRB5" # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "$option") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$value" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "$option\\>" "$AUDISP_REMOTE_CONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/$option\\>.*/$escaped_formatted_output/gi" "$AUDISP_REMOTE_CONFIG" else if [[ -s "$AUDISP_REMOTE_CONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDISP_REMOTE_CONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDISP_REMOTE_CONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDISP_REMOTE_CONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi Configure auditd to use audispd's syslog plugin To configure the auditd service to use the syslog plug-in of the audispd audit event multiplexor, set the active line in /etc/audit/plugins.d/syslog.conf to yes. Restart the auditd service: $ sudo service auditd restart 1 11 12 13 14 15 16 19 3 4 5 6 7 8 5.4.1.1 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 3.3.1 164.308(a)(1)(ii)(D) 164.308(a)(5)(ii)(B) 164.308(a)(5)(ii)(C) 164.308(a)(6)(ii) 164.308(a)(8) 164.310(d)(2)(iii) 164.312(b) 164.314(a)(2)(i)(C) 164.314(a)(2)(iii) 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 AU-4(1) CM-6(a) DE.AE-3 DE.AE-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.5.3 SRG-OS-000479-GPOS-00224 SRG-OS-000342-GPOS-00133 10.3.3 10.3 The auditd service does not include the ability to send audit records to a centralized server for management directly. It does, however, include a plug-in for audit event multiplexor (audispd) to pass audit records to the local syslog server. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_syslog_active="yes" AUDISP_SYSLOGCONFIG=/etc/audit/plugins.d/syslog.conf # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^active") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_syslog_active" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^active\\>" "$AUDISP_SYSLOGCONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^active\\>.*/$escaped_formatted_output/gi" "$AUDISP_SYSLOGCONFIG" else if [[ -s "$AUDISP_SYSLOGCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDISP_SYSLOGCONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDISP_SYSLOGCONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDISP_SYSLOGCONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-4(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.3 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.3 - auditd_audispd_syslog_plugin_activated - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Enable syslog plugin ansible.builtin.lineinfile: dest: /etc/audit/plugins.d/syslog.conf regexp: ^active line: active = yes create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-4(1) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.5.3 - PCI-DSSv4-10.3 - PCI-DSSv4-10.3.3 - auditd_audispd_syslog_plugin_activated - configure_strategy - low_complexity - low_disruption - medium_severity - no_reboot_needed Configure auditd Disk Error Action on Disk Error The auditd service can be configured to take an action when there is a disk error. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting ACTION appropriately: disk_error_action = ACTION Set this value to single to cause the system to switch to single-user mode for corrective action. Acceptable values also include syslog, exec, single, and halt For certain systems, the need for availability outweighs the need to log all actions, and a different setting should be determined. Details regarding all possible values for ACTION are described in the auditd.conf man page. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 SRG-OS-000047-GPOS-00023 SRG-APP-000098-CTR-000185 SRG-APP-000099-CTR-000190 SRG-APP-000100-CTR-000195 SRG-APP-000100-CTR-000200 SRG-APP-000109-CTR-000215 SRG-APP-000290-CTR-000670 SRG-APP-000357-CTR-000800 6.3.2.3 Taking appropriate action in case of disk errors will minimize the possibility of losing audit records. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_error_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_disk_error_action # promote to variable set_fact: var_auditd_disk_error_action: !!str tags: - always - name: Configure auditd Disk Error Action on Disk Error ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: disk_error_action = {{ var_auditd_disk_error_action.split('|')[0] }} regexp: ^\s*disk_error_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_error_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd Disk Error Action on Disk Error The auditd service can be configured to take an action when there is a disk error. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting ACTION appropriately: disk_error_action = ACTION Set this value to single to cause the system to switch to single-user mode for corrective action. Acceptable values also include syslog, exec, single, and halt. For certain systems, the need for availability outweighs the need to log all actions, and a different setting should be determined. Details regarding all possible values for ACTION are described in the auditd.conf man page. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 SRG-OS-000047-GPOS-00023 Taking appropriate action in case of disk errors will minimize the possibility of losing audit records. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_error_action_stig - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_disk_error_action # promote to variable set_fact: var_auditd_disk_error_action: !!str tags: - always - name: Configure auditd Disk Error Action on Disk Error ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: disk_error_action = {{ var_auditd_disk_error_action }} regexp: ^\s*disk_error_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_error_action_stig - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd Disk Full Action when Disk Space Is Full The auditd service can be configured to take an action when disk space is running low but prior to running out of space completely. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting ACTION appropriately: disk_full_action = ACTION Set this value to single to cause the system to switch to single-user mode for corrective action. Acceptable values also include syslog, exec, single, and halt For certain systems, the need for availability outweighs the need to log all actions, and a different setting should be determined. Details regarding all possible values for ACTION are described in the auditd.conf man page. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 SRG-OS-000047-GPOS-00023 6.3.2.3 Taking appropriate action in case of a filled audit storage volume will minimize the possibility of losing audit records. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_full_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_disk_full_action # promote to variable set_fact: var_auditd_disk_full_action: !!str tags: - always - name: Configure auditd Disk Full Action when Disk Space Is Full ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: disk_full_action = {{ var_auditd_disk_full_action.split('|')[0] }} regexp: ^\s*disk_full_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_full_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd Disk Full Action when Disk Space Is Full The auditd service can be configured to take an action when disk space is running low but prior to running out of space completely. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting ACTION appropriately: disk_full_action = ACTION Set this value to single to cause the system to switch to single-user mode for corrective action. Acceptable values also include syslog, single, and halt. For certain systems, the need for availability outweighs the need to log all actions, and a different setting should be determined. Details regarding all possible values for ACTION are described in the auditd.conf man page. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 SRG-OS-000047-GPOS-00023 Taking appropriate action in case of a filled audit storage volume will minimize the possibility of losing audit records. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_full_action_stig - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_disk_full_action # promote to variable set_fact: var_auditd_disk_full_action: !!str tags: - always - name: Configure auditd Disk Full Action when Disk Space Is Full ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: disk_full_action = {{ var_auditd_disk_full_action }} regexp: ^\s*disk_full_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - auditd_data_disk_full_action_stig - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd mail_acct Action on Low Disk Space The auditd service can be configured to send email to a designated account in certain situations. Add or correct the following line in /etc/audit/auditd.conf to ensure that administrators are notified via email for those situations: action_mail_acct = 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 5.4.1.1 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 3.3.1 164.312(a)(2)(ii) 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 CIP-003-8 R1.3 CIP-003-8 R3 CIP-003-8 R3.1 CIP-003-8 R3.2 CIP-003-8 R3.3 CIP-003-8 R5.1.1 CIP-003-8 R5.3 CIP-004-6 R2.2.3 CIP-004-6 R2.3 CIP-007-3 R5.1 CIP-007-3 R5.1.2 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 IA-5(1) AU-5(a) AU-5(2) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7.a SRG-OS-000046-GPOS-00022 SRG-OS-000343-GPOS-00134 6.3.2.4 Email sent to the root account is typically aliased to the administrators of the system, who can take appropriate action. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_action_mail_acct='' AUDITCONFIG=/etc/audit/auditd.conf # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^action_mail_acct") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_action_mail_acct" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^action_mail_acct\\>" "$AUDITCONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^action_mail_acct\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG" else if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDITCONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1) - PCI-DSS-Req-10.7.a - auditd_data_retention_action_mail_acct - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_action_mail_acct # promote to variable set_fact: var_auditd_action_mail_acct: !!str tags: - always - name: Configure auditd mail_acct Action on Low Disk Space - Configure auditd mail_acct Action on Low Disk Space ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf regexp: ^action_mail_acct line: action_mail_acct = {{ var_auditd_action_mail_acct }} state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(a) - NIST-800-53-CM-6(a) - NIST-800-53-IA-5(1) - PCI-DSS-Req-10.7.a - auditd_data_retention_action_mail_acct - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Configure auditd admin_space_left Action on Low Disk Space The auditd service can be configured to take an action when disk space is running low but prior to running out of space completely. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting ACTION appropriately: admin_space_left_action = ACTION Set this value to single to cause the system to switch to single user mode for corrective action. Acceptable values also include suspend and halt. For certain systems, the need for availability outweighs the need to log all actions, and a different setting should be determined. Details regarding all possible values for ACTION are described in the auditd.conf man page. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 5.4.1.1 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 3.3.1 164.312(a)(2)(ii) 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 SRG-OS-000343-GPOS-00134 6.3.2.4 10.5.1 10.5 Administrators should be made aware of an inability to record audit records. If a separate partition or logical volume of adequate size is used, running low on space for audit records should never occur. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_admin_space_left_action='' var_auditd_admin_space_left_action="$(echo $var_auditd_admin_space_left_action | cut -d \| -f 1)" AUDITCONFIG=/etc/audit/auditd.conf # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^admin_space_left_action") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_admin_space_left_action" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^admin_space_left_action\\>" "$AUDITCONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^admin_space_left_action\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG" else if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDITCONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - PCI-DSSv4-10.5 - PCI-DSSv4-10.5.1 - auditd_data_retention_admin_space_left_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_admin_space_left_action # promote to variable set_fact: var_auditd_admin_space_left_action: !!str tags: - always - name: Configure auditd admin_space_left Action on Low Disk Space ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: admin_space_left_action = {{ var_auditd_admin_space_left_action .split('|')[0] }} regexp: ^\s*admin_space_left_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - PCI-DSSv4-10.5 - PCI-DSSv4-10.5.1 - auditd_data_retention_admin_space_left_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd admin_space_left on Low Disk Space The auditd service can be configured to take an action when disk space is running low but prior to running out of space completely. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting PERCENTAGE appropriately: admin_space_left = PERCENTAGE% Set this value to to cause the system to perform an action. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 SRG-OS-000343-GPOS-00134 Notifying administrators of an impending disk space problem may allow them to take corrective action prior to any disruption. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_admin_space_left_percentage='' grep -q "^admin_space_left[[:space:]]*=.*$" /etc/audit/auditd.conf && \ sed -i "s/^admin_space_left[[:space:]]*=.*$/admin_space_left = $var_auditd_admin_space_left_percentage%/g" /etc/audit/auditd.conf || \ echo "admin_space_left = $var_auditd_admin_space_left_percentage%" >> /etc/audit/auditd.conf else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_admin_space_left_percentage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_admin_space_left_percentage # promote to variable set_fact: var_auditd_admin_space_left_percentage: !!str tags: - always - name: Configure auditd admin_space_left on Low Disk Space ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: admin_space_left = {{ var_auditd_admin_space_left_percentage }}% regexp: ^\s*admin_space_left\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_admin_space_left_percentage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Configure auditd flush priority The auditd service can be configured to synchronously write audit event data to disk. Add or correct the following line in /etc/audit/auditd.conf to ensure that audit event data is fully synchronized with the log files on the disk: flush = 1 12 13 14 15 16 2 3 5 6 7 8 9 APO10.01 APO10.03 APO10.04 APO10.05 APO11.04 BAI03.05 DSS01.03 DSS03.05 DSS05.02 DSS05.04 DSS05.05 DSS05.07 MEA01.01 MEA01.02 MEA01.03 MEA01.04 MEA01.05 MEA02.01 3.3.1 164.308(a)(1)(ii)(D) 164.308(a)(3)(ii)(A) 164.308(a)(5)(ii)(C) 164.312(a)(2)(i) 164.312(b) 164.312(d) 164.312(e) 4.3.2.6.7 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 6.2 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.14.2.7 A.15.2.1 A.15.2.2 CIP-004-6 R2.2.3 CIP-004-6 R3.3 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 CIP-007-3 R6.5 AU-11 CM-6(a) DE.CM-1 DE.CM-3 DE.CM-7 ID.SC-4 PR.PT-1 FAU_GEN.1 SRG-OS-000480-GPOS-00227 Audit data should be synchronously written to disk to ensure log integrity. These parameters assure that all audit event data is fully synchronized with the log files on the disk. - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-171-3.3.1 - NIST-800-53-AU-11 - NIST-800-53-CM-6(a) - auditd_data_retention_flush - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_flush # promote to variable set_fact: var_auditd_flush: !!str tags: - always - name: Configure auditd Flush Priority ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf regexp: ^\s*flush\s*=\s*.*$ line: flush = {{ var_auditd_flush }} state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-171-3.3.1 - NIST-800-53-AU-11 - NIST-800-53-CM-6(a) - auditd_data_retention_flush - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd Max Log File Size Determine the amount of audit data (in megabytes) which should be retained in each log file. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting the correct value of for STOREMB: max_log_file = STOREMB Set the value to 6 (MB) or higher for general-purpose systems. Larger values, of course, support retention of even more audit data. 1 11 12 13 14 15 16 19 3 4 5 6 7 8 5.4.1.1 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 CIP-004-6 R2.2.3 CIP-004-6 R3.3 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 CIP-007-3 R6.5 AU-11 CM-6(a) DE.AE-3 DE.AE-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 6.3.2.1 The total storage for audit log files must be large enough to retain log information over the period required. This is a function of the maximum log file size and the number of logs retained. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_max_log_file='' AUDITCONFIG=/etc/audit/auditd.conf # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^max_log_file") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_max_log_file" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^max_log_file\\>" "$AUDITCONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^max_log_file\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG" else if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDITCONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-53-AU-11 - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_max_log_file - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_max_log_file # promote to variable set_fact: var_auditd_max_log_file: !!str tags: - always - name: Configure auditd Max Log File Size ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf regexp: ^\s*max_log_file\s*=\s*.*$ line: max_log_file = {{ var_auditd_max_log_file }} state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-53-AU-11 - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_max_log_file - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd max_log_file_action Upon Reaching Maximum Log Size The default action to take when the logs reach their maximum size is to rotate the log files, discarding the oldest one. To configure the action taken by auditd, add or correct the line in /etc/audit/auditd.conf: max_log_file_action = ACTION Possible values for ACTION are described in the auditd.conf man page. These include: ignoresyslogsuspendrotatekeep_logs Set the ACTION to . The setting is case-insensitive. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 5.4.1.1 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 164.312(a)(2)(ii) 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 SRG-OS-000047-GPOS-00023 6.3.2.2 Automatically rotating logs (by setting this to rotate) minimizes the chances of the system unexpectedly running out of disk space by being overwhelmed with log data. However, for systems that must never discard log data, or which use external processes to transfer it and reclaim space, keep_logs can be employed. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_max_log_file_action='' AUDITCONFIG=/etc/audit/auditd.conf # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^max_log_file_action") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_max_log_file_action" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^max_log_file_action\\>" "$AUDITCONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^max_log_file_action\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG" else if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDITCONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_max_log_file_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_max_log_file_action # promote to variable set_fact: var_auditd_max_log_file_action: !!str tags: - always - name: Configure auditd max_log_file_action Upon Reaching Maximum Log Size ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: max_log_file_action = {{ var_auditd_max_log_file_action }} regexp: ^\s*max_log_file_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_max_log_file_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd max_log_file_action Upon Reaching Maximum Log Size The default action to take when the logs reach their maximum size is to rotate the log files, discarding the oldest one. To configure the action taken by auditd, add or correct the line in /etc/audit/auditd.conf: max_log_file_action = ACTION Possible values for ACTION are described in the auditd.conf man page. These include: ignoresyslogsuspendrotatekeep_logs Set the ACTION to rotate to ensure log rotation occurs. This is the default. The setting is case-insensitive. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 164.312(a)(2)(ii) 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 SRG-OS-000047-GPOS-00023 SRG-APP-000098-CTR-000185 SRG-APP-000099-CTR-000190 SRG-APP-000100-CTR-000195 SRG-APP-000100-CTR-000200 SRG-APP-000109-CTR-000215 SRG-APP-000290-CTR-000670 SRG-APP-000357-CTR-000800 Automatically rotating logs (by setting this to rotate) minimizes the chances of the system unexpectedly running out of disk space by being overwhelmed with log data. However, for systems that must never discard log data, or which use external processes to transfer it and reclaim space, keep_logs can be employed. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_max_log_file_action='' # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^max_log_file_action") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_max_log_file_action" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^max_log_file_action\\>" "/etc/audit/auditd.conf"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^max_log_file_action\\>.*/$escaped_formatted_output/gi" "/etc/audit/auditd.conf" else if [[ -s "/etc/audit/auditd.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/audit/auditd.conf" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/audit/auditd.conf" fi printf '%s\n' "$formatted_output" >> "/etc/audit/auditd.conf" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_max_log_file_action_stig - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_max_log_file_action # promote to variable set_fact: var_auditd_max_log_file_action: !!str tags: - always - name: Configure auditd max_log_file_action Upon Reaching Maximum Log Size ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: max_log_file_action = {{ var_auditd_max_log_file_action }} regexp: ^\s*max_log_file_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_max_log_file_action_stig - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd Number of Logs Retained Determine how many log files auditd should retain when it rotates logs. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting NUMLOGS with the correct value of : num_logs = NUMLOGS Set the value to 5 for general-purpose systems. Note that values less than 2 result in no log rotation. 1 11 12 13 14 15 16 19 3 4 5 6 7 8 5.4.1.1 APO11.04 APO12.06 BAI03.05 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 3.3.1 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 CIP-004-6 R2.2.3 CIP-004-6 R3.3 CIP-007-3 R5.2 CIP-007-3 R5.3.1 CIP-007-3 R5.3.2 CIP-007-3 R5.3.3 CIP-007-3 R6.5 AU-11 CM-6(a) DE.AE-3 DE.AE-5 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 The total storage for audit log files must be large enough to retain log information over the period required. This is a function of the maximum log file size and the number of logs retained. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_num_logs='' AUDITCONFIG=/etc/audit/auditd.conf # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^num_logs") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_num_logs" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^num_logs\\>" "$AUDITCONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^num_logs\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG" else if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDITCONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-11 - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_num_logs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_num_logs # promote to variable set_fact: var_auditd_num_logs: !!str tags: - always - name: Configure auditd Number of Logs Retained ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: num_logs = {{ var_auditd_num_logs }} regexp: ^\s*num_logs\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-11 - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_num_logs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd space_left Action on Low Disk Space The auditd service can be configured to take an action when disk space starts to run low. Edit the file /etc/audit/auditd.conf. Modify the following line, substituting ACTION appropriately: space_left_action = ACTION Possible values for ACTION are described in the auditd.conf man page. These include: syslogemailexecsuspendsinglehalt Set this to email (instead of the default, which is suspend) as it is more likely to get prompt attention. Acceptable values also include suspend, single, and halt. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 5.4.1.1 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 3.3.1 164.312(a)(2)(ii) 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 SRG-OS-000343-GPOS-00134 6.3.2.4 10.5.1 10.5 Notifying administrators of an impending disk space problem may allow them to take corrective action prior to any disruption. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_space_left_action='' var_auditd_space_left_action="$(echo $var_auditd_space_left_action | cut -d \| -f 1)" # # If space_left_action present in /etc/audit/auditd.conf, change value # to var_auditd_space_left_action, else # add "space_left_action = $var_auditd_space_left_action" to /etc/audit/auditd.conf # AUDITCONFIG=/etc/audit/auditd.conf # Strip any search characters in the key arg so that the key can be replaced without # adding any search characters to the config file. stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^space_left_action") # shellcheck disable=SC2059 printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_space_left_action" # If the key exists, change it. Otherwise, add it to the config_file. # We search for the key string followed by a word boundary (matched by \>), # so if we search for 'setting', 'setting2' won't match. if LC_ALL=C grep -q -m 1 -i -e "^space_left_action\\>" "$AUDITCONFIG"; then escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output") LC_ALL=C sed -i --follow-symlinks "s/^space_left_action\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG" else if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG" fi printf '%s\n' "$formatted_output" >> "$AUDITCONFIG" fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - PCI-DSSv4-10.5 - PCI-DSSv4-10.5.1 - auditd_data_retention_space_left_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_space_left_action # promote to variable set_fact: var_auditd_space_left_action: !!str tags: - always - name: Configure auditd space_left Action on Low Disk Space ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: space_left_action = {{ var_auditd_space_left_action.split('|')[0] }} regexp: ^\s*space_left_action\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - CJIS-5.4.1.1 - NIST-800-171-3.3.1 - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - PCI-DSSv4-10.5 - PCI-DSSv4-10.5.1 - auditd_data_retention_space_left_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Configure auditd space_left on Low Disk Space The auditd service can be configured to take an action when disk space is running low but prior to running out of space completely. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting PERCENTAGE appropriately: space_left = PERCENTAGE% Set this value to at least 25 to cause the system to notify the user of an issue. 1 11 12 13 14 15 16 19 2 3 4 5 6 7 8 APO11.04 APO12.06 APO13.01 BAI03.05 BAI04.04 BAI08.02 DSS02.02 DSS02.04 DSS02.07 DSS03.01 DSS05.04 DSS05.07 MEA02.01 4.2.3.10 4.3.3.3.9 4.3.3.5.8 4.3.4.4.7 4.3.4.5.6 4.3.4.5.7 4.3.4.5.8 4.4.2.1 4.4.2.2 4.4.2.4 SR 2.10 SR 2.11 SR 2.12 SR 2.8 SR 2.9 SR 6.1 SR 7.1 SR 7.2 A.12.1.3 A.12.4.1 A.12.4.2 A.12.4.3 A.12.4.4 A.12.7.1 A.16.1.4 A.16.1.5 A.16.1.7 A.17.2.1 AU-5(b) AU-5(2) AU-5(1) AU-5(4) CM-6(a) DE.AE-3 DE.AE-5 PR.DS-4 PR.PT-1 RS.AN-1 RS.AN-4 Req-10.7 SRG-OS-000343-GPOS-00134 Notifying administrators of an impending disk space problem may allow them to take corrective action prior to any disruption. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_space_left_percentage='' grep -q "^space_left[[:space:]]*=.*$" /etc/audit/auditd.conf && \ sed -i "s/^space_left[[:space:]]*=.*$/space_left = $var_auditd_space_left_percentage%/g" /etc/audit/auditd.conf || \ echo "space_left = $var_auditd_space_left_percentage%" >> /etc/audit/auditd.conf else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_space_left_percentage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_space_left_percentage # promote to variable set_fact: var_auditd_space_left_percentage: !!str tags: - always - name: Configure auditd space_left on Low Disk Space ansible.builtin.lineinfile: dest: /etc/audit/auditd.conf line: space_left = {{ var_auditd_space_left_percentage }}% regexp: ^\s*space_left\s*=\s*.*$ state: present create: true when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-5(1) - NIST-800-53-AU-5(2) - NIST-800-53-AU-5(4) - NIST-800-53-AU-5(b) - NIST-800-53-CM-6(a) - PCI-DSS-Req-10.7 - auditd_data_retention_space_left_percentage - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Set number of records to cause an explicit flush to audit logs To configure Audit daemon to issue an explicit flush to disk command after writing records, set freq to in /etc/audit/auditd.conf. CM-6 FAU_GEN.1 SRG-OS-000051-GPOS-00024 If option freq isn't set to , the flush to disk may happen after higher number of records, increasing the danger of audit loss. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_freq='' if [ -e "/etc/audit/auditd.conf" ] ; then LC_ALL=C sed -i "/^\s*freq\s*=\s*/Id" "/etc/audit/auditd.conf" else touch "/etc/audit/auditd.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/audit/auditd.conf" cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak" # Insert at the end of the file printf '%s\n' "freq = $var_auditd_freq" >> "/etc/audit/auditd.conf" # Clean up after ourselves. rm "/etc/audit/auditd.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - auditd_freq - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_freq # promote to variable set_fact: var_auditd_freq: !!str tags: - always - name: Set number of records to cause an explicit flush to audit logs block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*freq\s*=\s* state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*freq\s*=\s* state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*freq\s*=\s* line: freq = {{ var_auditd_freq }} state: present when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - auditd_freq - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Include Local Events in Audit Logs To configure Audit daemon to include local events in Audit logs, set local_events to yes in /etc/audit/auditd.conf. This is the default setting. CM-6 SRG-OS-000062-GPOS-00031 SRG-OS-000480-GPOS-00227 If option local_events isn't set to yes only events from network will be aggregated. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if [ -e "/etc/audit/auditd.conf" ] ; then LC_ALL=C sed -i "/^\s*local_events\s*=\s*/Id" "/etc/audit/auditd.conf" else touch "/etc/audit/auditd.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/audit/auditd.conf" cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak" # Insert at the end of the file printf '%s\n' "local_events = yes" >> "/etc/audit/auditd.conf" # Clean up after ourselves. rm "/etc/audit/auditd.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - auditd_local_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Include Local Events in Audit Logs block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*local_events\s*=\s* state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*local_events\s*=\s* state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*local_events\s*=\s* line: local_events = yes state: present when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - auditd_local_events - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Resolve information before writing to audit logs To configure Audit daemon to resolve all uid, gid, syscall, architecture, and socket address information before writing the events to disk, set log_format to ENRICHED in /etc/audit/auditd.conf. CM-6 AU-3 FAU_GEN.1.2 SRG-OS-000255-GPOS-00096 SRG-OS-000480-GPOS-00227 SRG-APP-000096-CTR-000175 SRG-APP-000097-CTR-000180 SRG-APP-000098-CTR-000185 SRG-APP-000099-CTR-000190 SRG-APP-000100-CTR-000195 SRG-APP-000100-CTR-000200 SRG-APP-000109-CTR-000215 SRG-APP-000290-CTR-000670 SRG-APP-000357-CTR-000800 If option log_format isn't set to ENRICHED, the audit records will be stored in a format exactly as the kernel sends them. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if [ -e "/etc/audit/auditd.conf" ] ; then LC_ALL=C sed -i "/^\s*log_format\s*=\s*/Id" "/etc/audit/auditd.conf" else touch "/etc/audit/auditd.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/audit/auditd.conf" cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak" # Insert at the end of the file printf '%s\n' "log_format = ENRICHED" >> "/etc/audit/auditd.conf" # Clean up after ourselves. rm "/etc/audit/auditd.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-3 - NIST-800-53-CM-6 - auditd_log_format - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy - name: Resolve information before writing to audit logs block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*log_format\s*=\s* state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*log_format\s*=\s* state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*log_format\s*=\s* line: log_format = ENRICHED state: present when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-3 - NIST-800-53-CM-6 - auditd_log_format - low_complexity - low_disruption - low_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Set type of computer node name logging in audit logs To configure Audit daemon to use a unique identifier as computer node name in the audit events, set name_format to in /etc/audit/auditd.conf. Whenever the variable var_auditd_name_format uses a multiple value option, for example A|B|C, the first value will be used when remediating this rule. CM-6 AU-3 FAU_GEN.1.2 SRG-OS-000039-GPOS-00017 SRG-OS-000342-GPOS-00133 SRG-OS-000479-GPOS-00224 10.2.2 10.2 If option name_format is left at its default value of none, audit events from different computers may be hard to distinguish. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then var_auditd_name_format='' var_auditd_name_format="$(echo $var_auditd_name_format | cut -d \| -f 1)" if [ -e "/etc/audit/auditd.conf" ] ; then LC_ALL=C sed -i "/^\s*name_format\s*=\s*/Id" "/etc/audit/auditd.conf" else touch "/etc/audit/auditd.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/audit/auditd.conf" cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak" # Insert at the end of the file printf '%s\n' "name_format = $var_auditd_name_format" >> "/etc/audit/auditd.conf" # Clean up after ourselves. rm "/etc/audit/auditd.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-3 - NIST-800-53-CM-6 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.2 - auditd_name_format - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: XCCDF Value var_auditd_name_format # promote to variable set_fact: var_auditd_name_format: !!str tags: - always - name: Set type of computer node name logging in audit logs - Define Value to Be Used in the Remediation ansible.builtin.set_fact: auditd_name_format_split="{{ var_auditd_name_format.split('|')[0] }}" when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-3 - NIST-800-53-CM-6 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.2 - auditd_name_format - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Set type of computer node name logging in audit logs block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*name_format\s*=\s* state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*name_format\s*=\s* state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*name_format\s*=\s* line: name_format = {{ auditd_name_format_split }} state: present when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-3 - NIST-800-53-CM-6 - PCI-DSSv4-10.2 - PCI-DSSv4-10.2.2 - auditd_name_format - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true Appropriate Action Must be Setup When the Internal Audit Event Queue is Full The audit system should have an action setup in the event the internal event queue becomes full. To setup an overflow action edit /etc/audit/auditd.conf. Set overflow_action to one of the following values: syslog, single, halt. AU-4(1) SRG-OS-000342-GPOS-00133 SRG-OS-000479-GPOS-00224 The audit system should have an action setup in the event the internal event queue becomes full so that no data is lost. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if [ -e "/etc/audit/auditd.conf" ] ; then LC_ALL=C sed -i "/^\s*overflow_action\s*=\s*/Id" "/etc/audit/auditd.conf" else touch "/etc/audit/auditd.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/audit/auditd.conf" cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak" # Insert at the end of the file printf '%s\n' "overflow_action = syslog" >> "/etc/audit/auditd.conf" # Clean up after ourselves. rm "/etc/audit/auditd.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-4(1) - auditd_overflow_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Appropriate Action Must be Setup When the Internal Audit Event Queue is Full block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*overflow_action\s*=\s* state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*overflow_action\s*=\s* state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*overflow_action\s*=\s* line: overflow_action = syslog state: present when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-4(1) - auditd_overflow_action - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy Write Audit Logs to the Disk To configure Audit daemon to write Audit logs to the disk, set write_logs to yes in /etc/audit/auditd.conf. This is the default setting. CM-6 SRG-OS-000480-GPOS-00227 If write_logs isn't set to yes, the Audit logs will not be written to the disk. # Remediation is applicable only in certain platforms if rpm --quiet -q audit && rpm --quiet -q kernel; then if [ -e "/etc/audit/auditd.conf" ] ; then LC_ALL=C sed -i "/^\s*write_logs\s*=\s*/Id" "/etc/audit/auditd.conf" else touch "/etc/audit/auditd.conf" fi # make sure file has newline at the end sed -i -e '$a\' "/etc/audit/auditd.conf" cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak" # Insert at the end of the file printf '%s\n' "write_logs = yes" >> "/etc/audit/auditd.conf" # Clean up after ourselves. rm "/etc/audit/auditd.conf.bak" else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-CM-6 - auditd_write_logs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy - name: Write Audit Logs to the Disk block: - name: Check for duplicate values ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*write_logs\s*=\s* state: absent check_mode: true changed_when: false register: dupes - name: Deduplicate values from /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*write_logs\s*=\s* state: absent when: dupes.found is defined and dupes.found > 1 - name: Insert correct line to /etc/audit/auditd.conf ansible.builtin.lineinfile: path: /etc/audit/auditd.conf create: true regexp: (?i)(?i)^\s*write_logs\s*=\s* line: write_logs = yes state: present when: - '"audit" in ansible_facts.packages' - '"kernel" in ansible_facts.packages' tags: - NIST-800-53-CM-6 - auditd_write_logs - low_complexity - low_disruption - medium_severity - no_reboot_needed - restrict_strategy --- apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig spec: config: ignition: version: 3.1.0 storage: files: - contents: source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }} mode: 0640 path: /etc/audit/auditd.conf overwrite: true System Accounting with auditd The audit service provides substantial capabilities for recording system activities. This section deals with permissions of auditd related files. Verify that audit tools are owned by group root The Fedora operating system audit tools must have the proper ownership configured to protected against unauthorized access. Verify it by running the following command: $ stat -c "%n %G" /sbin/auditctl /sbin/aureport /sbin/ausearch /sbin/autrace /sbin/auditd /sbin/audispd /sbin/augenrules /sbin/auditctl root /sbin/aureport root /sbin/ausearch root /sbin/autrace root /sbin/auditd root /sbin/audispd root /sbin/augenrules root Audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators SRG-OS-000256-GPOS-00097 SRG-OS-000257-GPOS-00098 6.3.4.10 Protecting audit information also includes identifying and protecting the tools used to view and manipulate log data. Therefore, protecting audit tools is necessary to prevent unauthorized operation on audit information. Operating systems providing tools to interface with audit information will leverage user permissions and roles identifying the user accessing the tools and the corresponding rights the user enjoys to make access decisions regarding the access to audit tools. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newgroup="" if getent group "0" >/dev/null 2>&1; then newgroup="0" fi if [[ -z "${newgroup}" ]]; then >&2 echo "0 is not a defined group on the system" else if ! stat -c "%g %G" "/sbin/auditctl" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /sbin/auditctl fi if ! stat -c "%g %G" "/sbin/aureport" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /sbin/aureport fi if ! stat -c "%g %G" "/sbin/ausearch" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /sbin/ausearch fi if ! stat -c "%g %G" "/sbin/autrace" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /sbin/autrace fi if ! stat -c "%g %G" "/sbin/auditd" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /sbin/auditd fi if ! stat -c "%g %G" "/sbin/audispd" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /sbin/audispd fi if ! stat -c "%g %G" "/sbin/augenrules" | grep -E -w -q "0"; then chgrp --no-dereference "$newgroup" /sbin/augenrules fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_groupownership_audit_binaries_newgroup variable if represented by gid ansible.builtin.set_fact: file_groupownership_audit_binaries_newgroup: '0' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/auditctl ansible.builtin.stat: path: /sbin/auditctl register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /sbin/auditctl ansible.builtin.file: path: /sbin/auditctl follow: false group: '{{ file_groupownership_audit_binaries_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/aureport ansible.builtin.stat: path: /sbin/aureport register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /sbin/aureport ansible.builtin.file: path: /sbin/aureport follow: false group: '{{ file_groupownership_audit_binaries_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/ausearch ansible.builtin.stat: path: /sbin/ausearch register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /sbin/ausearch ansible.builtin.file: path: /sbin/ausearch follow: false group: '{{ file_groupownership_audit_binaries_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/autrace ansible.builtin.stat: path: /sbin/autrace register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /sbin/autrace ansible.builtin.file: path: /sbin/autrace follow: false group: '{{ file_groupownership_audit_binaries_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/auditd ansible.builtin.stat: path: /sbin/auditd register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /sbin/auditd ansible.builtin.file: path: /sbin/auditd follow: false group: '{{ file_groupownership_audit_binaries_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/audispd ansible.builtin.stat: path: /sbin/audispd register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /sbin/audispd ansible.builtin.file: path: /sbin/audispd follow: false group: '{{ file_groupownership_audit_binaries_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/augenrules ansible.builtin.stat: path: /sbin/augenrules register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure group owner on /sbin/augenrules ansible.builtin.file: path: /sbin/augenrules follow: false group: '{{ file_groupownership_audit_binaries_newgroup }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_groupownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that audit tools are owned by root The Fedora operating system audit tools must have the proper ownership configured to protected against unauthorized access. Verify it by running the following command: $ stat -c "%n %U" /sbin/auditctl /sbin/aureport /sbin/ausearch /sbin/autrace /sbin/auditd /sbin/audispd /sbin/augenrules /sbin/audisp-syslog /sbin/auditctl root /sbin/aureport root /sbin/ausearch root /sbin/autrace root /sbin/auditd root /sbin/audispd root /sbin/augenrules root Audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators SRG-OS-000256-GPOS-00097 SRG-OS-000257-GPOS-00098 6.3.4.9 Protecting audit information also includes identifying and protecting the tools used to view and manipulate log data. Therefore, protecting audit tools is necessary to prevent unauthorized operation on audit information. Operating systems providing tools to interface with audit information will leverage user permissions and roles identifying the user accessing the tools and the corresponding rights the user enjoys to make access decisions regarding the access to audit tools. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then newown="" if id "0" >/dev/null 2>&1; then newown="0" fi if [[ -z "$newown" ]]; then >&2 echo "0 is not a defined user on the system" else if ! stat -c "%u %U" "/sbin/auditctl" | grep -E -w -q "0"; then chown --no-dereference "$newown" /sbin/auditctl fi if ! stat -c "%u %U" "/sbin/aureport" | grep -E -w -q "0"; then chown --no-dereference "$newown" /sbin/aureport fi if ! stat -c "%u %U" "/sbin/ausearch" | grep -E -w -q "0"; then chown --no-dereference "$newown" /sbin/ausearch fi if ! stat -c "%u %U" "/sbin/autrace" | grep -E -w -q "0"; then chown --no-dereference "$newown" /sbin/autrace fi if ! stat -c "%u %U" "/sbin/auditd" | grep -E -w -q "0"; then chown --no-dereference "$newown" /sbin/auditd fi if ! stat -c "%u %U" "/sbin/audispd" | grep -E -w -q "0"; then chown --no-dereference "$newown" /sbin/audispd fi if ! stat -c "%u %U" "/sbin/augenrules" | grep -E -w -q "0"; then chown --no-dereference "$newown" /sbin/augenrules fi fi else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set the file_ownership_audit_binaries_newown variable if represented by uid ansible.builtin.set_fact: file_ownership_audit_binaries_newown: '0' when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/auditctl ansible.builtin.stat: path: /sbin/auditctl register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /sbin/auditctl ansible.builtin.file: path: /sbin/auditctl follow: false owner: '{{ file_ownership_audit_binaries_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/aureport ansible.builtin.stat: path: /sbin/aureport register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /sbin/aureport ansible.builtin.file: path: /sbin/aureport follow: false owner: '{{ file_ownership_audit_binaries_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/ausearch ansible.builtin.stat: path: /sbin/ausearch register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /sbin/ausearch ansible.builtin.file: path: /sbin/ausearch follow: false owner: '{{ file_ownership_audit_binaries_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/autrace ansible.builtin.stat: path: /sbin/autrace register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /sbin/autrace ansible.builtin.file: path: /sbin/autrace follow: false owner: '{{ file_ownership_audit_binaries_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/auditd ansible.builtin.stat: path: /sbin/auditd register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /sbin/auditd ansible.builtin.file: path: /sbin/auditd follow: false owner: '{{ file_ownership_audit_binaries_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/audispd ansible.builtin.stat: path: /sbin/audispd register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /sbin/audispd ansible.builtin.file: path: /sbin/audispd follow: false owner: '{{ file_ownership_audit_binaries_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/augenrules ansible.builtin.stat: path: /sbin/augenrules register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure owner on /sbin/augenrules ansible.builtin.file: path: /sbin/augenrules follow: false owner: '{{ file_ownership_audit_binaries_newown }}' when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_ownership_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify that audit tools Have Mode 0755 or less The Fedora operating system audit tools must have the proper permissions configured to protected against unauthorized access. Verify it by running the following command: $ stat -c "%n %a" /sbin/auditctl /sbin/aureport /sbin/ausearch /sbin/autrace /sbin/auditd /sbin/audispd /sbin/augenrules /sbin/auditctl 755 /sbin/aureport 755 /sbin/ausearch 755 /sbin/autrace 755 /sbin/auditd 755 /sbin/audispd 755 /sbin/augenrules 755 Audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators SRG-OS-000256-GPOS-00097 SRG-OS-000257-GPOS-00098 6.3.4.8 Protecting audit information also includes identifying and protecting the tools used to view and manipulate log data. Therefore, protecting audit tools is necessary to prevent unauthorized operation on audit information. Operating systems providing tools to interface with audit information will leverage user permissions and roles identifying the user accessing the tools and the corresponding rights the user enjoys to make access decisions regarding the access to audit tools. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then chmod u-s,g-ws,o-wt /sbin/auditctl chmod u-s,g-ws,o-wt /sbin/aureport chmod u-s,g-ws,o-wt /sbin/ausearch chmod u-s,g-ws,o-wt /sbin/autrace chmod u-s,g-ws,o-wt /sbin/auditd chmod u-s,g-ws,o-wt /sbin/audispd chmod u-s,g-ws,o-wt /sbin/augenrules else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/auditctl ansible.builtin.stat: path: /sbin/auditctl register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-ws,o-wt on /sbin/auditctl ansible.builtin.file: path: /sbin/auditctl mode: u-s,g-ws,o-wt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/aureport ansible.builtin.stat: path: /sbin/aureport register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-ws,o-wt on /sbin/aureport ansible.builtin.file: path: /sbin/aureport mode: u-s,g-ws,o-wt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/ausearch ansible.builtin.stat: path: /sbin/ausearch register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-ws,o-wt on /sbin/ausearch ansible.builtin.file: path: /sbin/ausearch mode: u-s,g-ws,o-wt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/autrace ansible.builtin.stat: path: /sbin/autrace register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-ws,o-wt on /sbin/autrace ansible.builtin.file: path: /sbin/autrace mode: u-s,g-ws,o-wt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/auditd ansible.builtin.stat: path: /sbin/auditd register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-ws,o-wt on /sbin/auditd ansible.builtin.file: path: /sbin/auditd mode: u-s,g-ws,o-wt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/audispd ansible.builtin.stat: path: /sbin/audispd register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-ws,o-wt on /sbin/audispd ansible.builtin.file: path: /sbin/audispd mode: u-s,g-ws,o-wt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /sbin/augenrules ansible.builtin.stat: path: /sbin/augenrules register: file_exists when: '"kernel" in ansible_facts.packages' tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-s,g-ws,o-wt on /sbin/augenrules ansible.builtin.file: path: /sbin/augenrules mode: u-s,g-ws,o-wt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - configure_strategy - file_permissions_audit_binaries - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /etc/audit/auditd.conf To properly set the permissions of /etc/audit/auditd.conf, run the command: $ sudo chmod 0640 /etc/audit/auditd.conf AU-12(b) SRG-OS-000063-GPOS-00032 Without the capability to restrict the roles and individuals that can select which events are audited, unauthorized personnel may be able to prevent the auditing of critical events. Misconfigured audits may degrade the system's performance by overwhelming the audit log. Misconfigured audits may also make it more difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then chmod u-xs,g-xws,o-xwrt /etc/audit/auditd.conf else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(b) - configure_strategy - file_permissions_etc_audit_auditd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Test for existence /etc/audit/auditd.conf ansible.builtin.stat: path: /etc/audit/auditd.conf register: file_exists when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(b) - configure_strategy - file_permissions_etc_audit_auditd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Ensure permission u-xs,g-xws,o-xwrt on /etc/audit/auditd.conf ansible.builtin.file: path: /etc/audit/auditd.conf mode: u-xs,g-xws,o-xwrt when: - '"kernel" in ansible_facts.packages' - file_exists.stat is defined and file_exists.stat.exists tags: - NIST-800-53-AU-12(b) - configure_strategy - file_permissions_etc_audit_auditd - low_complexity - low_disruption - medium_severity - no_reboot_needed Verify Permissions on /etc/audit/rules.d/*.rules To properly set the permissions of /etc/audit/rules.d/*.rules, run the command: $ sudo chmod 0600 /etc/audit/rules.d/*.rules AU-12(b) SRG-OS-000063-GPOS-00032 Without the capability to restrict the roles and individuals that can select which events are audited, unauthorized personnel may be able to prevent the auditing of critical events. Misconfigured audits may degrade the system's performance by overwhelming the audit log. Misconfigured audits may also make it more difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. # Remediation is applicable only in certain platforms if rpm --quiet -q kernel; then find -P /etc/audit/rules.d/ -maxdepth 1 -perm /u+xs,g+xwrs,o+xwrt -type f -regextype posix-extended -regex '^.*rules$' -exec chmod u-xs,g-xwrs,o-xwrt {} \; else >&2 echo 'Remediation is not applicable, nothing was done' fi - name: Gather the package facts package_facts: manager: auto tags: - NIST-800-53-AU-12(b) - configure_strategy - file_permissions_etc_audit_rulesd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Find /etc/audit/rules.d/ file(s) ansible.builtin.command: find -P /etc/audit/rules.d/ -maxdepth 1 -perm /u+xs,g+xwrs,o+xwrt -type f -regextype posix-extended -regex "^.*rules$" register: files_found changed_when: false failed_when: false check_mode: false when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(b) - configure_strategy - file_permissions_etc_audit_rulesd - low_complexity - low_disruption - medium_severity - no_reboot_needed - name: Set permissions for /etc/audit/rules.d/ file(s) ansible.builtin.file: path: '{{ item }}' mode: u-xs,g-xwrs,o-xwrt state: file with_items: - '{{ files_found.stdout_lines }}' when: '"kernel" in ansible_facts.packages' tags: - NIST-800-53-AU-12(b) - configure_strategy - file_permissions_etc_audit_rulesd - low_complexity - low_disruption - medium_severity - no_reboot_needed OSCAP Scan Result claiveapa claiveapa 127.0.0.1 192.168.1.10 192.168.1.7 0:0:0:0:0:0:0:1 2001:4454:377:ac00:c68f:7bd4:c895:28ef fe80:0:0:0:2a0b:823a:126f:e9fe 2001:4454:377:ac00:572a:a063:78b8:ebda fe80:0:0:0:874c:da25:2d77:e538 OpenSCAP 1.4.2 claiveapa claiveapa 00:00:00:00:00:00 00:00:00:00:00:00 E6:98:E4:0C:32:43 E6:98:E4:0C:32:43 D0:37:45:A6:50:75 D0:37:45:A6:50:75 127.0.0.1 192.168.1.10 192.168.1.7 ::1 2001:4454:377:ac00:c68f:7bd4:c895:28ef fe80::2a0b:823a:126f:e9fe 2001:4454:377:ac00:572a:a063:78b8:ebda fe80::874c:da25:2d77:e538 DEFAULT 900 0 /var/log/sudo.log 5 minimal Authorized users only. All activity may be monitored and reported. ^Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ ^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ ^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$ SHA512 sha512 5 3 /var/log/faillock 900 0 5 requisite -1 1 8 -1 4 3 3 15 -1 3 -1 default ^(root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody|tss|systemd-coredump|dbus|polkitd|avahi|colord|rtkit|pipewire|clevis|sssd|geoclue|flatpak|setroubleshoot|libstoragemgmt|systemd-oom|gdm|cockpit-ws|cockpit-wsinstance|gnome-initial-setup|sshd|chrony|dnsmasq|tcpdump|admin)$ 35 90 7 15 7 5000 sugroup 4 1 600 ^(\.bashrc|\.zshrc|\.cshrc|\.profile|\.bash_login|\.bash_profile)$ 027 flush full 500 prctl sha512 certs/signing_key.pem 0 logcollector 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 0 0 1 1 0 0 1 1 1 2 /dev/cdrom 022 1 P targeted enforcing loopback-only smtp.$mydomain change_me@localhost 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org 10 changemero changemerw aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,3des-cbc,aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se hmac-sha2-512,hmac-sha2-256,hmac-sha1,hmac-sha1-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com 300 4 0 ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256 hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-ripemd160 10 0 512M 1h no sandbox 60 10:30:100 sha1 300 180 logcollector root single 5 single single data 50 6 rotate hostname 5 100 email 25 notselected notapplicable notapplicable notselected notapplicable notselected notselected notselected notselected notselected notselected notapplicable notapplicable notselected notapplicable notapplicable notapplicable notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notapplicable notapplicable notselected notselected notselected notselected notapplicable notselected notapplicable notselected notselected notselected notselected notapplicable notapplicable notselected notselected notselected notselected notselected notapplicable notapplicable notselected notselected notselected notapplicable notselected notselected notselected notapplicable notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notapplicable notapplicable notapplicable notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notapplicable notselected notselected notapplicable notapplicable notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notapplicable notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notapplicable notapplicable notapplicable notapplicable notselected notapplicable notapplicable notapplicable notapplicable notapplicable notapplicable notapplicable notapplicable notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notapplicable notapplicable notapplicable notapplicable notapplicable notselected notselected notapplicable notselected notselected notselected notselected notapplicable notapplicable notselected notselected notapplicable notapplicable notselected notapplicable notapplicable notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected notselected 0.000000